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" />
+
, "container":
@@ -24,6 +25,7 @@ exports[`Column Resizer renders the structure correctly 1`] = ` class="column-resizer-bar" />
+
, "debug": [Function], "findAllByAltText": [Function], From acace57a071684cb8ea98e0fa5c53bf43c6837f1 Mon Sep 17 00:00:00 2001 From: Yordan Date: Fri, 21 Nov 2025 10:25:26 +0100 Subject: [PATCH 08/19] refactor: fix failing test --- .../__tests__/__snapshots__/ColumnResizer.spec.tsx.snap | 2 -- packages/pluggableWidgets/datagrid-web/src/utils/test-utils.tsx | 1 - 2 files changed, 3 deletions(-) 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 531e05370c..35e61c875c 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,7 +13,6 @@ exports[`Column Resizer renders the structure correctly 1`] = ` class="column-resizer-bar" />
-
, "container":
@@ -25,7 +24,6 @@ exports[`Column Resizer renders the structure correctly 1`] = ` class="column-resizer-bar" />
- , "debug": [Function], "findAllByAltText": [Function], diff --git a/packages/pluggableWidgets/datagrid-web/src/utils/test-utils.tsx b/packages/pluggableWidgets/datagrid-web/src/utils/test-utils.tsx index a2f4d9c646..006aa771f8 100644 --- a/packages/pluggableWidgets/datagrid-web/src/utils/test-utils.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/utils/test-utils.tsx @@ -2,7 +2,6 @@ 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 => { From e7152553b3f11449121ead00d91a9a1434cd7342 Mon Sep 17 00:00:00 2001 From: Yordan Date: Fri, 21 Nov 2025 15:19:22 +0100 Subject: [PATCH 09/19] feat: enhance drag-and-drop functionality with DragHandle component --- .../src/components/ColumnContainer.tsx | 64 ++++++++++++++++--- .../src/components/ColumnHeader.tsx | 1 - 2 files changed, 56 insertions(+), 9 deletions(-) diff --git a/packages/pluggableWidgets/datagrid-web/src/components/ColumnContainer.tsx b/packages/pluggableWidgets/datagrid-web/src/components/ColumnContainer.tsx index e537707f67..9193037d13 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/ColumnContainer.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/ColumnContainer.tsx @@ -1,5 +1,5 @@ import classNames from "classnames"; -import { HTMLAttributes, KeyboardEvent, ReactElement, ReactNode, useMemo } from "react"; +import { DragEventHandler, HTMLAttributes, KeyboardEvent, ReactElement, ReactNode, useMemo } from "react"; import { FaArrowsAltV } from "./icons/FaArrowsAltV"; import { FaLongArrowAltDown } from "./icons/FaLongArrowAltDown"; import { FaLongArrowAltUp } from "./icons/FaLongArrowAltUp"; @@ -16,6 +16,11 @@ export interface ColumnContainerProps { isLast?: boolean; resizer: ReactElement; } +interface DragHandleProps { + draggable: boolean; + onDragStart?: DragEventHandler; + onDragEnd?: DragEventHandler; +} export const ColumnContainer = observer(function ColumnContainer(props: ColumnContainerProps): ReactElement { const { columnsFilterable, id: gridId } = useDatagridConfig(); @@ -56,13 +61,7 @@ export const ColumnContainer = observer(function ColumnContainer(props: ColumnCo onDragEnter={draggableProps.onDragEnter} onDragOver={draggableProps.onDragOver} > -
+
+ {draggableProps.draggable && ( + + )} + + {caption.length > 0 ? caption : "\u00a0"} + {canSort ? : null} {columnsFilterable && ( @@ -83,6 +92,45 @@ export const ColumnContainer = observer(function ColumnContainer(props: ColumnCo ); }); +function DragHandle({ draggable, onDragStart, onDragEnd }: DragHandleProps): ReactElement { + const handleMouseDown = (e: React.MouseEvent) => { + // Only stop propagation, don't prevent default - we need default for drag to work + e.stopPropagation(); + }; + + const handleClick = (e: React.MouseEvent) => { + // Stop click events from bubbling to prevent sorting + e.stopPropagation(); + e.preventDefault(); + }; + + const handleDragStart = (e: React.DragEvent) => { + // Don't stop propagation here - let the drag start properly + if (onDragStart) { + onDragStart(e); + } + }; + + const handleDragEnd = (e: React.DragEvent) => { + if (onDragEnd) { + onDragEnd(e); + } + }; + + return ( + + â ¿ + + ); +} + function SortIcon(): ReactNode { const column = useColumn(); switch (column.sortDir) { diff --git a/packages/pluggableWidgets/datagrid-web/src/components/ColumnHeader.tsx b/packages/pluggableWidgets/datagrid-web/src/components/ColumnHeader.tsx index a27a18e7af..710d244461 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/ColumnHeader.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/ColumnHeader.tsx @@ -22,7 +22,6 @@ export default function ColumnHeader(props: ColumnHeaderProps): ReactElement { {...props.sortProps} aria-label={props.canSort ? "sort " + props.caption : props.caption} > - {props.caption.length > 0 ? props.caption : "\u00a0"} {props.children}
); From c3f116dc357fd27d7b613be3b451b1c2d6b370fa Mon Sep 17 00:00:00 2001 From: Yordan Date: Fri, 21 Nov 2025 15:31:35 +0100 Subject: [PATCH 10/19] refactor: fix lint errors --- .../datawidgets/web/_datagrid.scss | 18 ++++++++++++++++++ .../src/components/ColumnContainer.tsx | 19 ++++++++++++++----- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss b/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss index 79b2ac631b..1e0be5f270 100644 --- a/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss +++ b/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss @@ -131,6 +131,24 @@ $root: ".widget-datagrid"; align-self: center; } + /* Drag handle */ + .drag-handle { + cursor: grab; + pointer-events: auto; + position: relative; + width: 14px; + padding: 0; + flex-grow: 0; + display: flex; + justify-content: center; + z-index: 1; + + &:hover { + background-color: var(--brand-primary-50, $brand-light); + color: var(--brand-primary, $brand-primary); + } + } + &:focus:not(:focus-visible) { outline: none; } diff --git a/packages/pluggableWidgets/datagrid-web/src/components/ColumnContainer.tsx b/packages/pluggableWidgets/datagrid-web/src/components/ColumnContainer.tsx index 9193037d13..bd18dcebc9 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/ColumnContainer.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/ColumnContainer.tsx @@ -1,5 +1,14 @@ import classNames from "classnames"; -import { DragEventHandler, HTMLAttributes, KeyboardEvent, ReactElement, ReactNode, useMemo } from "react"; +import { + DragEvent, + DragEventHandler, + HTMLAttributes, + KeyboardEvent, + MouseEvent, + ReactElement, + ReactNode, + useMemo +} from "react"; import { FaArrowsAltV } from "./icons/FaArrowsAltV"; import { FaLongArrowAltDown } from "./icons/FaLongArrowAltDown"; import { FaLongArrowAltUp } from "./icons/FaLongArrowAltUp"; @@ -93,25 +102,25 @@ export const ColumnContainer = observer(function ColumnContainer(props: ColumnCo }); function DragHandle({ draggable, onDragStart, onDragEnd }: DragHandleProps): ReactElement { - const handleMouseDown = (e: React.MouseEvent) => { + const handleMouseDown = (e: MouseEvent): void => { // Only stop propagation, don't prevent default - we need default for drag to work e.stopPropagation(); }; - const handleClick = (e: React.MouseEvent) => { + const handleClick = (e: MouseEvent): void => { // Stop click events from bubbling to prevent sorting e.stopPropagation(); e.preventDefault(); }; - const handleDragStart = (e: React.DragEvent) => { + const handleDragStart = (e: DragEvent): void => { // Don't stop propagation here - let the drag start properly if (onDragStart) { onDragStart(e); } }; - const handleDragEnd = (e: React.DragEvent) => { + const handleDragEnd = (e: DragEvent): void => { if (onDragEnd) { onDragEnd(e); } From 878d2eab72321561b8ab388813831059a44e42c4 Mon Sep 17 00:00:00 2001 From: Yordan Stoyanov Date: Thu, 27 Nov 2025 16:31:18 +0100 Subject: [PATCH 11/19] refactor: standardize use of brandi --- .../datawidgets/web/_datagrid.scss | 4 + .../src/components/ColumnContainer.tsx | 159 ++--------------- .../src/components/ColumnHeader.tsx | 116 ++++++++++-- .../src/components/__tests__/Header.spec.tsx | 168 ++++++++++++++++++ ...wModel.ts => HeaderDragnDrop.viewModel.ts} | 34 ++-- ...c.ts => HeaderDragnDrop.viewModel.spec.ts} | 24 +-- .../model/containers/Datagrid.container.ts | 10 +- .../src/model/hooks/injection-hooks.ts | 2 +- .../datagrid-web/src/model/tokens.ts | 5 +- 9 files changed, 332 insertions(+), 190 deletions(-) create mode 100644 packages/pluggableWidgets/datagrid-web/src/components/__tests__/Header.spec.tsx rename packages/pluggableWidgets/datagrid-web/src/features/column/{ColumnHeader.viewModel.ts => HeaderDragnDrop.viewModel.ts} (79%) rename packages/pluggableWidgets/datagrid-web/src/features/column/__tests__/{ColumnHeader.viewModel.spec.ts => HeaderDragnDrop.viewModel.spec.ts} (95%) diff --git a/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss b/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss index 1e0be5f270..1f2b489f0b 100644 --- a/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss +++ b/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss @@ -149,6 +149,10 @@ $root: ".widget-datagrid"; } } + .drag-handle + .column-caption { + padding-inline-start: 4px; + } + &:focus:not(:focus-visible) { outline: none; } diff --git a/packages/pluggableWidgets/datagrid-web/src/components/ColumnContainer.tsx b/packages/pluggableWidgets/datagrid-web/src/components/ColumnContainer.tsx index bd18dcebc9..ac8d9c1d0a 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/ColumnContainer.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/ColumnContainer.tsx @@ -1,163 +1,58 @@ import classNames from "classnames"; -import { - DragEvent, - DragEventHandler, - HTMLAttributes, - KeyboardEvent, - MouseEvent, - 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 { ReactElement } from "react"; +import { ColumnHeader } from "./ColumnHeader"; +import { useColumn, useColumnsStore, useDatagridConfig, useColumnHeaderVM } from "../model/hooks/injection-hooks"; import { ColumnResizerProps } from "./ColumnResizer"; -import { ColumnHeaderViewModel } from "../features/column/ColumnHeader.viewModel"; import { observer } from "mobx-react-lite"; export interface ColumnContainerProps { isLast?: boolean; resizer: ReactElement; } -interface DragHandleProps { - draggable: boolean; - onDragStart?: DragEventHandler; - onDragEnd?: DragEventHandler; -} 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 { columnFilters } = useColumnsStore(); + const { canSort, columnId, columnIndex, canResize, sortDir, header } = useColumn(); + const { draggableProps, dropTarget, dragging } = useColumnHeaderVM(); - 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(); + const caption = header.trim(); return (
-
- - {draggableProps.draggable && ( - - )} - - {caption.length > 0 ? caption : "\u00a0"} - - {canSort ? : null} - +
+ {columnsFilterable && ( -
- {columnsStore.columnFilters[column.columnIndex]?.renderFilterWidgets()} +
+ {columnFilters[columnIndex]?.renderFilterWidgets()}
)}
- {column.canResize ? props.resizer : null} + {canResize ? props.resizer : null}
); }); -function DragHandle({ draggable, onDragStart, onDragEnd }: DragHandleProps): ReactElement { - const handleMouseDown = (e: MouseEvent): void => { - // Only stop propagation, don't prevent default - we need default for drag to work - e.stopPropagation(); - }; - - const handleClick = (e: MouseEvent): void => { - // Stop click events from bubbling to prevent sorting - e.stopPropagation(); - e.preventDefault(); - }; - - const handleDragStart = (e: DragEvent): void => { - // Don't stop propagation here - let the drag start properly - if (onDragStart) { - onDragStart(e); - } - }; - - const handleDragEnd = (e: DragEvent): void => { - if (onDragEnd) { - onDragEnd(e); - } - }; - - return ( - - â ¿ - - ); -} - -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 { +function getAriaSort(canSort: boolean, sortDir: string | undefined): "ascending" | "descending" | "none" | undefined { if (!canSort) { return undefined; } - switch (column.sortDir) { + switch (sortDir) { case "asc": return "ascending"; case "desc": @@ -166,19 +61,3 @@ function getAriaSort(canSort: boolean, column: GridColumn): "ascending" | "desce 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 index 710d244461..ec01888225 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/ColumnHeader.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/ColumnHeader.tsx @@ -1,28 +1,106 @@ 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"; +import { DragEventHandler, DragEvent, HTMLAttributes, KeyboardEvent, MouseEvent, ReactElement, ReactNode } from "react"; +import { FaArrowsAltV } from "./icons/FaArrowsAltV"; +import { FaLongArrowAltDown } from "./icons/FaLongArrowAltDown"; +import { FaLongArrowAltUp } from "./icons/FaLongArrowAltUp"; +import { useColumn, useColumnHeaderVM } from "../model/hooks/injection-hooks"; +import { observer } from "mobx-react-lite"; + +interface DragHandleProps { + draggable: boolean; + onDragStart?: DragEventHandler; + onDragEnd?: DragEventHandler; } -export default function ColumnHeader(props: ColumnHeaderProps): ReactElement { +export const ColumnHeader = observer(function ColumnHeader(): ReactElement { + const { draggableProps, dragging } = useColumnHeaderVM(); + const { header, canSort, alignment, toggleSort } = useColumn(); + const caption = header.trim(); + const sortProps = canSort ? getSortProps(toggleSort) : null; + return (
- {props.children} + {draggableProps.draggable && ( + + )} + {caption.length > 0 ? caption : "\u00a0"} + {canSort ? : null}
); +}); + +function DragHandle({ draggable, onDragStart, onDragEnd }: DragHandleProps): ReactElement { + const handleMouseDown = (e: MouseEvent): void => { + // Only stop propagation, don't prevent default - we need default for drag to work + e.stopPropagation(); + }; + + const handleClick = (e: MouseEvent): void => { + // Stop click events from bubbling to prevent sorting + e.stopPropagation(); + e.preventDefault(); + }; + + const handleDragStart = (e: DragEvent): void => { + // Don't stop propagation here - let the drag start properly + if (onDragStart) { + onDragStart(e); + } + }; + + const handleDragEnd = (e: DragEvent): void => { + if (onDragEnd) { + onDragEnd(e); + } + }; + + return ( + + â ¿ + + ); +} + +function SortIcon(): ReactNode { + const column = useColumn(); + switch (column.sortDir) { + case "asc": + return ; + case "desc": + return ; + default: + return ; + } +} + +function getSortProps(toggleSort: () => void): HTMLAttributes { + return { + onClick: () => { + toggleSort(); + }, + onKeyDown: (e: KeyboardEvent) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + toggleSort(); + } + }, + role: "button", + tabIndex: 0 + }; } diff --git a/packages/pluggableWidgets/datagrid-web/src/components/__tests__/Header.spec.tsx b/packages/pluggableWidgets/datagrid-web/src/components/__tests__/Header.spec.tsx new file mode 100644 index 0000000000..63167f0fe9 --- /dev/null +++ b/packages/pluggableWidgets/datagrid-web/src/components/__tests__/Header.spec.tsx @@ -0,0 +1,168 @@ +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 { ColumnContainer } from "../ColumnContainer"; +import { ColumnResizer } from "../ColumnResizer"; + +describe("ColumnContainer", () => { + 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(); + }); +}); diff --git a/packages/pluggableWidgets/datagrid-web/src/features/column/ColumnHeader.viewModel.ts b/packages/pluggableWidgets/datagrid-web/src/features/column/HeaderDragnDrop.viewModel.ts similarity index 79% rename from packages/pluggableWidgets/datagrid-web/src/features/column/ColumnHeader.viewModel.ts rename to packages/pluggableWidgets/datagrid-web/src/features/column/HeaderDragnDrop.viewModel.ts index 27be4adede..775e0621da 100644 --- a/packages/pluggableWidgets/datagrid-web/src/features/column/ColumnHeader.viewModel.ts +++ b/packages/pluggableWidgets/datagrid-web/src/features/column/HeaderDragnDrop.viewModel.ts @@ -1,22 +1,20 @@ import { makeAutoObservable } from "mobx"; import { DragEvent, DragEventHandler } from "react"; import { HeaderDragnDropStore } from "./HeaderDragnDrop.store"; -import { ColumnId } from "../../typings/GridColumn"; +import { ColumnId, GridColumn } 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; +export class HeaderDragnDropViewModel { + constructor( + private dndStore: HeaderDragnDropStore, + private columnsStore: ColumnGroupStore, + private config: { columnsDraggable: boolean }, + private column: GridColumn + ) { makeAutoObservable(this); } @@ -28,6 +26,10 @@ export class ColumnHeaderViewModel { return this.dndStore.isDragging; } + get isDraggable(): boolean { + return this.config.columnsDraggable && this.column.canDrag; + } + /** Handlers exposed to the component. */ get draggableProps(): { draggable?: boolean; @@ -37,7 +39,7 @@ export class ColumnHeaderViewModel { onDragEnter?: DragEventHandler; onDragEnd?: DragEventHandler; } { - if (!this.columnsDraggable) { + if (!this.isDraggable) { return {}; } return { @@ -50,7 +52,7 @@ export class ColumnHeaderViewModel { }; } - private handleDragStart = (e: DragEvent): void => { + handleDragStart = (e: DragEvent): void => { const elt = (e.target as HTMLDivElement).closest(".th") as HTMLDivElement; if (!elt) { return; @@ -61,7 +63,7 @@ export class ColumnHeaderViewModel { this.dndStore.setIsDragging([columnAtTheLeft, columnId, columnAtTheRight]); }; - private handleDragOver = (e: DragEvent): void => { + handleDragOver = (e: DragEvent): void => { const dragging = this.dragging; if (!dragging) { return; @@ -93,15 +95,15 @@ export class ColumnHeaderViewModel { } }; - private handleDragEnter = (e: DragEvent): void => { + handleDragEnter = (e: DragEvent): void => { e.preventDefault(); }; - private handleDragEnd = (): void => { + handleDragEnd = (): void => { this.dndStore.clearDragState(); }; - private handleOnDrop = (_e: DragEvent): void => { + handleOnDrop = (_e: DragEvent): void => { const dragging = this.dragging; const dropTarget = this.dropTarget; this.handleDragEnd(); diff --git a/packages/pluggableWidgets/datagrid-web/src/features/column/__tests__/ColumnHeader.viewModel.spec.ts b/packages/pluggableWidgets/datagrid-web/src/features/column/__tests__/HeaderDragnDrop.viewModel.spec.ts similarity index 95% rename from packages/pluggableWidgets/datagrid-web/src/features/column/__tests__/ColumnHeader.viewModel.spec.ts rename to packages/pluggableWidgets/datagrid-web/src/features/column/__tests__/HeaderDragnDrop.viewModel.spec.ts index bb8b0dde39..91490e3e39 100644 --- a/packages/pluggableWidgets/datagrid-web/src/features/column/__tests__/ColumnHeader.viewModel.spec.ts +++ b/packages/pluggableWidgets/datagrid-web/src/features/column/__tests__/HeaderDragnDrop.viewModel.spec.ts @@ -1,40 +1,42 @@ import { DragEvent } from "react"; -import { ColumnHeaderViewModel } from "../ColumnHeader.viewModel"; +import { HeaderDragnDropViewModel } from "../HeaderDragnDrop.viewModel"; import { HeaderDragnDropStore } from "../HeaderDragnDrop.store"; import { ColumnId } from "../../../typings/GridColumn"; describe("ColumnHeaderViewModel", () => { let dndStore: HeaderDragnDropStore; let mockColumnsStore: any; + let mockColumn: any; beforeEach(() => { dndStore = new HeaderDragnDropStore(); mockColumnsStore = { swapColumns: jest.fn() }; + mockColumn = { + canDrag: true, + columnId: "col1" as ColumnId + }; }); describe("when columnsDraggable is false", () => { it("returns empty draggableProps", () => { - const vm = new ColumnHeaderViewModel({ + const vm = new HeaderDragnDropViewModel( dndStore, - columnsStore: mockColumnsStore, - columnsDraggable: false - }); + mockColumnsStore, + { columnsDraggable: false }, + mockColumn + ); expect(vm.draggableProps).toEqual({}); }); }); describe("when columnsDraggable is true", () => { - let vm: ColumnHeaderViewModel; + let vm: HeaderDragnDropViewModel; beforeEach(() => { - vm = new ColumnHeaderViewModel({ - dndStore, - columnsStore: mockColumnsStore, - columnsDraggable: true - }); + vm = new HeaderDragnDropViewModel(dndStore, mockColumnsStore, { columnsDraggable: true }, mockColumn); }); it("returns draggable props with handlers", () => { 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 3ca4e2208a..41e47856f9 100644 --- a/packages/pluggableWidgets/datagrid-web/src/model/containers/Datagrid.container.ts +++ b/packages/pluggableWidgets/datagrid-web/src/model/containers/Datagrid.container.ts @@ -31,7 +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 { HeaderDragnDropViewModel } from "../../features/column/HeaderDragnDrop.viewModel"; import { GridBasicData } from "../../helpers/state/GridBasicData"; import { GridPersonalizationStore } from "../../helpers/state/GridPersonalizationStore"; import { DatagridConfig } from "../configs/Datagrid.config"; @@ -107,6 +107,9 @@ injected( DG.selectionCounterCfg.optional ); +// drag and drop +injected(HeaderDragnDropViewModel, DG.headerDragDrop, CORE.columnsStore, CORE.config, CORE.column); + export class DatagridContainer extends Container { id = `DatagridContainer@${generateUUID()}`; constructor(root: Container) { @@ -118,7 +121,7 @@ export class DatagridContainer extends Container { // Columns store this.bind(CORE.columnsStore).toInstance(ColumnGroupStore).inSingletonScope(); // Drag and Drop store - this.bind(DG.headerDragDrop).toInstance(HeaderDragnDropStore).inSingletonScope(); + this.bind(DG.columnHeaderVM).toInstance(HeaderDragnDropViewModel).inSingletonScope(); // Query service this.bind(DG.query).toInstance(DatasourceService).inSingletonScope(); // Grid sizing and scrolling store @@ -201,6 +204,9 @@ export class DatagridContainer extends Container { // Config this.bind(CORE.config).toConstant(config); + // Columns draggable setting + this.bind(DG.columnsDraggable).toConstant(config.columnsDraggable); + // Connect select all module this.bind(SA_TOKENS.progressService).toConstant(selectAllModule.get(SA_TOKENS.progressService)); this.bind(SA_TOKENS.selectionDialogVM).toConstant(selectAllModule.get(SA_TOKENS.selectionDialogVM)); 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 e17c4432b9..f25c8a5ac0 100644 --- a/packages/pluggableWidgets/datagrid-web/src/model/hooks/injection-hooks.ts +++ b/packages/pluggableWidgets/datagrid-web/src/model/hooks/injection-hooks.ts @@ -23,7 +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 [useColumnHeaderVM] = createInjectionHooks(DG.columnHeaderVM); 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 3f1e877fe1..8246e82777 100644 --- a/packages/pluggableWidgets/datagrid-web/src/model/tokens.ts +++ b/packages/pluggableWidgets/datagrid-web/src/model/tokens.ts @@ -39,6 +39,7 @@ 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 { HeaderDragnDropViewModel } from "../features/column/HeaderDragnDrop.viewModel"; import { DatasourceParamsController } from "../model/services/DatasourceParamsController"; import { GridColumn } from "../typings/GridColumn"; import { DatagridConfig } from "./configs/Datagrid.config"; @@ -147,7 +148,9 @@ export const DG_TOKENS = { clickActionHelper: token("@service:ClickActionHelper"), focusService: token("@service:FocusTargetController"), checkboxEventsHandler: token("@service:CheckboxEventsController"), - headerDragDrop: token("HeaderDragnDropStore"), + headerDragDrop: token("@store:HeaderDragnDropStore"), + columnsDraggable: token("@const:columnsDraggable"), + columnHeaderVM: token("ColumnHeaderViewModel"), cellEventsHandler: token("@service:CellEventsController") }; From 2e30233350d770c374bf4458addc70c953096018 Mon Sep 17 00:00:00 2001 From: Yordan Stoyanov Date: Fri, 28 Nov 2025 14:48:57 +0100 Subject: [PATCH 12/19] refactor: improve naming, consistency and clean up --- .../src/components/ColumnContainer.tsx | 17 ++++++------- .../src/components/ColumnHeader.tsx | 12 +++------ .../features/column/HeaderDragnDrop.store.ts | 5 ++++ .../column/HeaderDragnDrop.viewModel.ts | 25 ++----------------- .../model/containers/Datagrid.container.ts | 9 ++++--- .../src/model/hooks/injection-hooks.ts | 2 +- .../datagrid-web/src/model/tokens.ts | 3 +-- 7 files changed, 26 insertions(+), 47 deletions(-) diff --git a/packages/pluggableWidgets/datagrid-web/src/components/ColumnContainer.tsx b/packages/pluggableWidgets/datagrid-web/src/components/ColumnContainer.tsx index ac8d9c1d0a..af29f9ad20 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/ColumnContainer.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/ColumnContainer.tsx @@ -14,30 +14,29 @@ export const ColumnContainer = observer(function ColumnContainer(props: ColumnCo const { columnsFilterable, id: gridId } = useDatagridConfig(); const { columnFilters } = useColumnsStore(); const { canSort, columnId, columnIndex, canResize, sortDir, header } = useColumn(); - const { draggableProps, dropTarget, dragging } = useColumnHeaderVM(); - + const vm = useColumnHeaderVM(); const caption = header.trim(); return (
{columnsFilterable && ( -
+
{columnFilters[columnIndex]?.renderFilterWidgets()}
)} diff --git a/packages/pluggableWidgets/datagrid-web/src/components/ColumnHeader.tsx b/packages/pluggableWidgets/datagrid-web/src/components/ColumnHeader.tsx index ec01888225..6c0a5436be 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/ColumnHeader.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/ColumnHeader.tsx @@ -13,24 +13,20 @@ interface DragHandleProps { } export const ColumnHeader = observer(function ColumnHeader(): ReactElement { - const { draggableProps, dragging } = useColumnHeaderVM(); const { header, canSort, alignment, toggleSort } = useColumn(); const caption = header.trim(); const sortProps = canSort ? getSortProps(toggleSort) : null; + const vm = useColumnHeaderVM(); return (
- {draggableProps.draggable && ( - + {vm.isDraggable && ( + )} {caption.length > 0 ? caption : "\u00a0"} {canSort ? : null} diff --git a/packages/pluggableWidgets/datagrid-web/src/features/column/HeaderDragnDrop.store.ts b/packages/pluggableWidgets/datagrid-web/src/features/column/HeaderDragnDrop.store.ts index 14090ab1ae..54fc13b982 100644 --- a/packages/pluggableWidgets/datagrid-web/src/features/column/HeaderDragnDrop.store.ts +++ b/packages/pluggableWidgets/datagrid-web/src/features/column/HeaderDragnDrop.store.ts @@ -1,6 +1,11 @@ import { action, makeAutoObservable } from "mobx"; import { ColumnId } from "../../typings/GridColumn"; +/** + * MobX store for managing drag & drop state of column headers. + * Tracks which column is being dragged and where it can be dropped. + * @injectable + */ export class HeaderDragnDropStore { private _dragOver: [ColumnId, "before" | "after"] | undefined = undefined; private _isDragging: [ColumnId | undefined, ColumnId, ColumnId | undefined] | undefined = undefined; diff --git a/packages/pluggableWidgets/datagrid-web/src/features/column/HeaderDragnDrop.viewModel.ts b/packages/pluggableWidgets/datagrid-web/src/features/column/HeaderDragnDrop.viewModel.ts index 775e0621da..fed47eca29 100644 --- a/packages/pluggableWidgets/datagrid-web/src/features/column/HeaderDragnDrop.viewModel.ts +++ b/packages/pluggableWidgets/datagrid-web/src/features/column/HeaderDragnDrop.viewModel.ts @@ -1,5 +1,5 @@ import { makeAutoObservable } from "mobx"; -import { DragEvent, DragEventHandler } from "react"; +import { DragEvent } from "react"; import { HeaderDragnDropStore } from "./HeaderDragnDrop.store"; import { ColumnId, GridColumn } from "../../typings/GridColumn"; import { ColumnGroupStore } from "../../helpers/state/ColumnGroupStore"; @@ -7,6 +7,7 @@ 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. + * @injectable */ export class HeaderDragnDropViewModel { constructor( @@ -30,28 +31,6 @@ export class HeaderDragnDropViewModel { return this.config.columnsDraggable && this.column.canDrag; } - /** Handlers exposed to the component. */ - get draggableProps(): { - draggable?: boolean; - onDragStart?: DragEventHandler; - onDragOver?: DragEventHandler; - onDrop?: DragEventHandler; - onDragEnter?: DragEventHandler; - onDragEnd?: DragEventHandler; - } { - if (!this.isDraggable) { - return {}; - } - return { - draggable: true, - onDragStart: this.handleDragStart, - onDragOver: this.handleDragOver, - onDrop: this.handleOnDrop, - onDragEnter: this.handleDragEnter, - onDragEnd: this.handleDragEnd - }; - } - handleDragStart = (e: DragEvent): void => { const elt = (e.target as HTMLDivElement).closest(".th") as HTMLDivElement; if (!elt) { 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 41e47856f9..5524f296b3 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 { HeaderDragnDropViewModel } from "../../features/column/HeaderDragnDrop.viewModel"; import { GridBasicData } from "../../helpers/state/GridBasicData"; import { GridPersonalizationStore } from "../../helpers/state/GridPersonalizationStore"; @@ -108,6 +109,7 @@ injected( ); // drag and drop +injected(HeaderDragnDropStore); injected(HeaderDragnDropViewModel, DG.headerDragDrop, CORE.columnsStore, CORE.config, CORE.column); export class DatagridContainer extends Container { @@ -121,7 +123,9 @@ export class DatagridContainer extends Container { // Columns store this.bind(CORE.columnsStore).toInstance(ColumnGroupStore).inSingletonScope(); // Drag and Drop store - this.bind(DG.columnHeaderVM).toInstance(HeaderDragnDropViewModel).inSingletonScope(); + this.bind(DG.headerDragDrop).toInstance(HeaderDragnDropStore).inSingletonScope(); + // Drag and Drop view model + this.bind(DG.headerDragnDropVM).toInstance(HeaderDragnDropViewModel).inSingletonScope(); // Query service this.bind(DG.query).toInstance(DatasourceService).inSingletonScope(); // Grid sizing and scrolling store @@ -204,9 +208,6 @@ export class DatagridContainer extends Container { // Config this.bind(CORE.config).toConstant(config); - // Columns draggable setting - this.bind(DG.columnsDraggable).toConstant(config.columnsDraggable); - // Connect select all module this.bind(SA_TOKENS.progressService).toConstant(selectAllModule.get(SA_TOKENS.progressService)); this.bind(SA_TOKENS.selectionDialogVM).toConstant(selectAllModule.get(SA_TOKENS.selectionDialogVM)); 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 f25c8a5ac0..c9d531b37e 100644 --- a/packages/pluggableWidgets/datagrid-web/src/model/hooks/injection-hooks.ts +++ b/packages/pluggableWidgets/datagrid-web/src/model/hooks/injection-hooks.ts @@ -23,7 +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 [useColumnHeaderVM] = createInjectionHooks(DG.columnHeaderVM); +export const [useColumnHeaderVM] = createInjectionHooks(DG.headerDragnDropVM); 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 8246e82777..1f45c3455e 100644 --- a/packages/pluggableWidgets/datagrid-web/src/model/tokens.ts +++ b/packages/pluggableWidgets/datagrid-web/src/model/tokens.ts @@ -149,8 +149,7 @@ export const DG_TOKENS = { focusService: token("@service:FocusTargetController"), checkboxEventsHandler: token("@service:CheckboxEventsController"), headerDragDrop: token("@store:HeaderDragnDropStore"), - columnsDraggable: token("@const:columnsDraggable"), - columnHeaderVM: token("ColumnHeaderViewModel"), + headerDragnDropVM: token("@viewmodel:ColumnHeaderViewModel"), cellEventsHandler: token("@service:CellEventsController") }; From 735370f0772acbe67a28381ea68de30350d360f8 Mon Sep 17 00:00:00 2001 From: Yordan Stoyanov Date: Mon, 1 Dec 2025 15:39:59 +0100 Subject: [PATCH 13/19] refactor: ensure consistent naming, prop destructuring, update tests --- .../datawidgets/web/_datagrid.scss | 3 + .../src/components/ColumnContainer.tsx | 7 +- .../src/components/ColumnHeader.tsx | 11 +- ...ader.spec.tsx => ColumnContainer.spec.tsx} | 0 .../ColumnContainer.spec.tsx.snap | 214 ++++++++++++++++++ .../HeaderDragnDrop.viewModel.spec.ts | 122 ++-------- .../src/model/hooks/injection-hooks.ts | 2 +- 7 files changed, 245 insertions(+), 114 deletions(-) rename packages/pluggableWidgets/datagrid-web/src/components/__tests__/{Header.spec.tsx => ColumnContainer.spec.tsx} (100%) create mode 100644 packages/pluggableWidgets/datagrid-web/src/components/__tests__/__snapshots__/ColumnContainer.spec.tsx.snap diff --git a/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss b/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss index 1f2b489f0b..a85745ef2c 100644 --- a/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss +++ b/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss @@ -147,6 +147,9 @@ $root: ".widget-datagrid"; background-color: var(--brand-primary-50, $brand-light); color: var(--brand-primary, $brand-primary); } + :active { + cursor: grabbing; + } } .drag-handle + .column-caption { diff --git a/packages/pluggableWidgets/datagrid-web/src/components/ColumnContainer.tsx b/packages/pluggableWidgets/datagrid-web/src/components/ColumnContainer.tsx index af29f9ad20..979dcbe4d6 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/ColumnContainer.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/ColumnContainer.tsx @@ -1,7 +1,7 @@ import classNames from "classnames"; import { ReactElement } from "react"; import { ColumnHeader } from "./ColumnHeader"; -import { useColumn, useColumnsStore, useDatagridConfig, useColumnHeaderVM } from "../model/hooks/injection-hooks"; +import { useColumn, useColumnsStore, useDatagridConfig, useHeaderDragnDropVM } from "../model/hooks/injection-hooks"; import { ColumnResizerProps } from "./ColumnResizer"; import { observer } from "mobx-react-lite"; @@ -13,8 +13,9 @@ export interface ColumnContainerProps { export const ColumnContainer = observer(function ColumnContainer(props: ColumnContainerProps): ReactElement { const { columnsFilterable, id: gridId } = useDatagridConfig(); const { columnFilters } = useColumnsStore(); - const { canSort, columnId, columnIndex, canResize, sortDir, header } = useColumn(); - const vm = useColumnHeaderVM(); + const column = useColumn(); + const { canSort, columnId, columnIndex, canResize, sortDir, header } = column; + const vm = useHeaderDragnDropVM(); const caption = header.trim(); return ( diff --git a/packages/pluggableWidgets/datagrid-web/src/components/ColumnHeader.tsx b/packages/pluggableWidgets/datagrid-web/src/components/ColumnHeader.tsx index 6c0a5436be..0ae6ee3e39 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/ColumnHeader.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/ColumnHeader.tsx @@ -1,9 +1,9 @@ import classNames from "classnames"; -import { DragEventHandler, DragEvent, HTMLAttributes, KeyboardEvent, MouseEvent, ReactElement, ReactNode } from "react"; +import { DragEvent, DragEventHandler, HTMLAttributes, KeyboardEvent, MouseEvent, ReactElement, ReactNode } from "react"; import { FaArrowsAltV } from "./icons/FaArrowsAltV"; import { FaLongArrowAltDown } from "./icons/FaLongArrowAltDown"; import { FaLongArrowAltUp } from "./icons/FaLongArrowAltUp"; -import { useColumn, useColumnHeaderVM } from "../model/hooks/injection-hooks"; +import { useColumn, useHeaderDragnDropVM } from "../model/hooks/injection-hooks"; import { observer } from "mobx-react-lite"; interface DragHandleProps { @@ -13,10 +13,11 @@ interface DragHandleProps { } export const ColumnHeader = observer(function ColumnHeader(): ReactElement { - const { header, canSort, alignment, toggleSort } = useColumn(); + const column = useColumn(); + const { header, canSort, alignment } = column; const caption = header.trim(); - const sortProps = canSort ? getSortProps(toggleSort) : null; - const vm = useColumnHeaderVM(); + const sortProps = canSort ? getSortProps(() => column.toggleSort()) : null; + const vm = useHeaderDragnDropVM(); return (
+
+
+
+ + Column 1 + +
+
+
+
+ +`; + +exports[`ColumnContainer renders the structure correctly when draggable 1`] = ` + +
+
+
+ + â ¿ + + + Column 1 + +
+
+
+
+ +`; + +exports[`ColumnContainer renders the structure correctly when filterable with custom filter 1`] = ` + +
+
+
+ + Column 1 + +
+
+
+ Custom filter +
+
+
+
+
+`; + +exports[`ColumnContainer renders the structure correctly when resizable 1`] = ` + +
+
+
+ + Column 1 + +
+
+
+
+ +`; + +exports[`ColumnContainer renders the structure correctly when sortable 1`] = ` + +
+
+
+ + Column 1 + + +
+
+
+
+ +`; + +exports[`ColumnContainer renders the structure correctly when value is empty 1`] = ` + +
+
+
+ +   + +
+
+
+
+ +`; diff --git a/packages/pluggableWidgets/datagrid-web/src/features/column/__tests__/HeaderDragnDrop.viewModel.spec.ts b/packages/pluggableWidgets/datagrid-web/src/features/column/__tests__/HeaderDragnDrop.viewModel.spec.ts index 91490e3e39..a11aa24123 100644 --- a/packages/pluggableWidgets/datagrid-web/src/features/column/__tests__/HeaderDragnDrop.viewModel.spec.ts +++ b/packages/pluggableWidgets/datagrid-web/src/features/column/__tests__/HeaderDragnDrop.viewModel.spec.ts @@ -20,7 +20,7 @@ describe("ColumnHeaderViewModel", () => { }); describe("when columnsDraggable is false", () => { - it("returns empty draggableProps", () => { + it("is not draggable", () => { const vm = new HeaderDragnDropViewModel( dndStore, mockColumnsStore, @@ -28,7 +28,7 @@ describe("ColumnHeaderViewModel", () => { mockColumn ); - expect(vm.draggableProps).toEqual({}); + expect(vm.isDraggable).toBe(false); }); }); @@ -39,37 +39,8 @@ describe("ColumnHeaderViewModel", () => { vm = new HeaderDragnDropViewModel(dndStore, mockColumnsStore, { columnsDraggable: true }, mockColumn); }); - 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"]); - }); + it("is draggable", () => { + expect(vm.isDraggable).toBe(true); }); describe("handleDragStart", () => { @@ -77,7 +48,7 @@ describe("ColumnHeaderViewModel", () => { const mockElement = createMockElement("col1", "col0", "col2"); const event = createMockDragEvent(mockElement); - vm.draggableProps.onDragStart?.(event); + vm.handleDragStart(event); expect(dndStore.isDragging).toEqual(["col0", "col1", "col2"]); }); @@ -86,7 +57,7 @@ describe("ColumnHeaderViewModel", () => { const mockElement = createMockElement("col1", undefined, "col2"); const event = createMockDragEvent(mockElement); - vm.draggableProps.onDragStart?.(event); + vm.handleDragStart(event); expect(dndStore.isDragging).toEqual([undefined, "col1", "col2"]); }); @@ -95,7 +66,7 @@ describe("ColumnHeaderViewModel", () => { const mockElement = createMockElement("col1", "col0", undefined); const event = createMockDragEvent(mockElement); - vm.draggableProps.onDragStart?.(event); + vm.handleDragStart(event); expect(dndStore.isDragging).toEqual(["col0", "col1", undefined]); }); @@ -107,7 +78,7 @@ describe("ColumnHeaderViewModel", () => { } } as any; - vm.draggableProps.onDragStart?.(event); + vm.handleDragStart(event); expect(dndStore.isDragging).toBeUndefined(); }); @@ -122,7 +93,7 @@ describe("ColumnHeaderViewModel", () => { dndStore.clearDragState(); const event = createMockDragOverEvent("col2", 100, 50); - vm.draggableProps.onDragOver?.(event); + vm.handleDragOver(event); expect(dndStore.dragOver).toBeUndefined(); }); @@ -130,7 +101,7 @@ describe("ColumnHeaderViewModel", () => { it("does nothing when columnId is missing", () => { const event = createMockDragOverEvent("", 100, 50); - vm.draggableProps.onDragOver?.(event); + vm.handleDragOver(event); expect(dndStore.dragOver).toBeUndefined(); }); @@ -139,7 +110,7 @@ describe("ColumnHeaderViewModel", () => { dndStore.setDragOver(["col2" as ColumnId, "after"]); const event = createMockDragOverEvent("col1", 100, 50); - vm.draggableProps.onDragOver?.(event); + vm.handleDragOver(event); expect(dndStore.dragOver).toBeUndefined(); }); @@ -147,7 +118,7 @@ describe("ColumnHeaderViewModel", () => { it("sets dropTarget to before when hovering over left sibling", () => { const event = createMockDragOverEvent("col0", 100, 50); - vm.draggableProps.onDragOver?.(event); + vm.handleDragOver(event); expect(dndStore.dragOver).toEqual(["col0", "before"]); }); @@ -155,51 +126,17 @@ describe("ColumnHeaderViewModel", () => { it("sets dropTarget to after when hovering over right sibling", () => { const event = createMockDragOverEvent("col2", 100, 50); - vm.draggableProps.onDragOver?.(event); + vm.handleDragOver(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); + vm.handleDragEnter(event); expect(event.preventDefault).toHaveBeenCalled(); }); @@ -210,7 +147,7 @@ describe("ColumnHeaderViewModel", () => { dndStore.setIsDragging(["col0" as ColumnId, "col1" as ColumnId, "col2" as ColumnId]); dndStore.setDragOver(["col2" as ColumnId, "after"]); - vm.draggableProps.onDragEnd?.({} as any); + vm.handleDragEnd(); expect(dndStore.isDragging).toBeUndefined(); expect(dndStore.dragOver).toBeUndefined(); @@ -222,7 +159,7 @@ describe("ColumnHeaderViewModel", () => { dndStore.setIsDragging(["col0" as ColumnId, "col1" as ColumnId, "col2" as ColumnId]); dndStore.setDragOver(["col3" as ColumnId, "after"]); - vm.draggableProps.onDrop?.({} as any); + vm.handleOnDrop({} as any); expect(mockColumnsStore.swapColumns).toHaveBeenCalledWith("col1", ["col3", "after"]); }); @@ -231,32 +168,7 @@ describe("ColumnHeaderViewModel", () => { 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); + vm.handleOnDrop({} as any); expect(dndStore.isDragging).toBeUndefined(); expect(dndStore.dragOver).toBeUndefined(); 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 c9d531b37e..8a5ae2451e 100644 --- a/packages/pluggableWidgets/datagrid-web/src/model/hooks/injection-hooks.ts +++ b/packages/pluggableWidgets/datagrid-web/src/model/hooks/injection-hooks.ts @@ -23,7 +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 [useColumnHeaderVM] = createInjectionHooks(DG.headerDragnDropVM); +export const [useHeaderDragnDropVM] = createInjectionHooks(DG.headerDragnDropVM); export const [useClickActionHelper] = createInjectionHooks(DG.clickActionHelper); export const [useFocusService] = createInjectionHooks(DG.focusService); export const [useCheckboxEventsHandler] = createInjectionHooks(DG.checkboxEventsHandler); From 2f94f20857d9731d7412afc556f38a920a8c682c Mon Sep 17 00:00:00 2001 From: Yordan Stoyanov Date: Tue, 2 Dec 2025 10:33:23 +0100 Subject: [PATCH 14/19] fix: update SelectActionHandler initialization to use null instead of undefined --- .../gallery-web/src/helpers/useItemSelectHelper.ts | 2 +- .../pluggableWidgets/gallery-web/src/utils/test-utils.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/pluggableWidgets/gallery-web/src/helpers/useItemSelectHelper.ts b/packages/pluggableWidgets/gallery-web/src/helpers/useItemSelectHelper.ts index 050128f4e0..2e291dbca8 100644 --- a/packages/pluggableWidgets/gallery-web/src/helpers/useItemSelectHelper.ts +++ b/packages/pluggableWidgets/gallery-web/src/helpers/useItemSelectHelper.ts @@ -6,5 +6,5 @@ export function useItemSelectHelper( selectionHelper: SelectionHelper | undefined ): SelectActionHandler { // eslint-disable-next-line react-hooks/exhaustive-deps - return useMemo(() => new SelectActionHandler(selection, selectionHelper), [selectionHelper]); + return useMemo(() => new SelectActionHandler(selection, selectionHelper ?? null), [selectionHelper]); } diff --git a/packages/pluggableWidgets/gallery-web/src/utils/test-utils.tsx b/packages/pluggableWidgets/gallery-web/src/utils/test-utils.tsx index 0d5ac84aa3..2caffacba8 100644 --- a/packages/pluggableWidgets/gallery-web/src/utils/test-utils.tsx +++ b/packages/pluggableWidgets/gallery-web/src/utils/test-utils.tsx @@ -81,7 +81,7 @@ export function createMockGalleryContext(): GalleryRootScope { refreshInterval: 0 }); - const mockSelectHelper = new SelectActionHandler("None", undefined); + const mockSelectHelper = new SelectActionHandler("None", null); return { rootStore: mockStore, @@ -110,7 +110,7 @@ type Mocks = { export function mockProps(params: Helpers & Mocks = {}): GalleryProps { const { onClick = undefined, - selectHelper = new SelectActionHandler("None", undefined), + selectHelper = new SelectActionHandler("None", null), actionHelper = new ClickActionHelper("single", onClick), focusController = new FocusTargetController(new PositionController(), new VirtualGridLayout(3, 4, 10)), itemEventsController = new ItemEventsController( From 50d1892274ea761a3cc51abeb421eaf0d20390a2 Mon Sep 17 00:00:00 2001 From: Yordan Stoyanov Date: Mon, 8 Dec 2025 18:26:30 +0100 Subject: [PATCH 15/19] fix: restore sort icon state, add dragndropdesign mode and react icon --- .../datawidgets/web/_datagrid.scss | 11 +++- .../src/Datagrid.editorPreview.tsx | 7 ++- .../src/components/ColumnHeader.tsx | 56 +++---------------- .../src/components/DragHandle.tsx | 46 +++++++++++++++ .../src/components/icons/FaGripVertical.tsx | 15 +++++ 5 files changed, 84 insertions(+), 51 deletions(-) create mode 100644 packages/pluggableWidgets/datagrid-web/src/components/DragHandle.tsx create mode 100644 packages/pluggableWidgets/datagrid-web/src/components/icons/FaGripVertical.tsx diff --git a/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss b/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss index a85745ef2c..850852725c 100644 --- a/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss +++ b/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss @@ -139,17 +139,24 @@ $root: ".widget-datagrid"; width: 14px; padding: 0; flex-grow: 0; + flex-shrink: 0; display: flex; justify-content: center; + align-self: normal; z-index: 1; &:hover { background-color: var(--brand-primary-50, $brand-light); - color: var(--brand-primary, $brand-primary); + svg { + color: var(--brand-primary, $brand-primary); + } } - :active { + &:active { cursor: grabbing; } + svg { + margin: 0; + } } .drag-handle + .column-caption { diff --git a/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorPreview.tsx b/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorPreview.tsx index c7e8f0b31a..fcba79bf21 100644 --- a/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorPreview.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorPreview.tsx @@ -4,9 +4,11 @@ import { GUID, ObjectItem } from "mendix"; import { Selectable } from "mendix/preview/Selectable"; import { createContext, CSSProperties, PropsWithChildren, ReactElement, ReactNode, useContext } from "react"; import { ColumnsPreviewType, DatagridPreviewProps } from "typings/DatagridProps"; +import { DragHandle } from "./components/DragHandle"; import { FaArrowsAltV } from "./components/icons/FaArrowsAltV"; import { FaEye } from "./components/icons/FaEye"; import { ColumnPreview } from "./helpers/ColumnPreview"; + import "./ui/DatagridPreview.scss"; declare module "mendix/preview/Selectable" { @@ -157,7 +159,7 @@ function GridHeader(): ReactNode { } function ColumnHeader({ column }: { column: ColumnsPreviewType }): ReactNode { - const { columnsFilterable, columnsSortable, columnsHidable } = useProps(); + const { columnsFilterable, columnsSortable, columnsHidable, columnsDraggable } = useProps(); const columnPreview = new ColumnPreview(column, 0); const caption = columnPreview.header; const canSort = columnsSortable && columnPreview.canSort; @@ -172,6 +174,9 @@ function ColumnHeader({ column }: { column: ColumnsPreviewType }): ReactNode { >
+ {columnsDraggable && ( + {}} onDragEnd={() => {}} /> + )} {caption.length > 0 ? caption : "\u00a0"} {canSort && }
diff --git a/packages/pluggableWidgets/datagrid-web/src/components/ColumnHeader.tsx b/packages/pluggableWidgets/datagrid-web/src/components/ColumnHeader.tsx index 0ae6ee3e39..024ab37b12 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/ColumnHeader.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/ColumnHeader.tsx @@ -1,15 +1,15 @@ import classNames from "classnames"; -import { DragEvent, DragEventHandler, HTMLAttributes, KeyboardEvent, MouseEvent, ReactElement, ReactNode } from "react"; +import { HTMLAttributes, KeyboardEvent, ReactElement, ReactNode } from "react"; +import { DragHandle } from "./DragHandle"; import { FaArrowsAltV } from "./icons/FaArrowsAltV"; import { FaLongArrowAltDown } from "./icons/FaLongArrowAltDown"; import { FaLongArrowAltUp } from "./icons/FaLongArrowAltUp"; import { useColumn, useHeaderDragnDropVM } from "../model/hooks/injection-hooks"; import { observer } from "mobx-react-lite"; +import { SortDirection } from "../typings/sorting"; -interface DragHandleProps { - draggable: boolean; - onDragStart?: DragEventHandler; - onDragEnd?: DragEventHandler; +interface SortIconProps { + direction: SortDirection | undefined; } export const ColumnHeader = observer(function ColumnHeader(): ReactElement { @@ -30,53 +30,13 @@ export const ColumnHeader = observer(function ColumnHeader(): ReactElement { )} {caption.length > 0 ? caption : "\u00a0"} - {canSort ? : null} + {canSort ? : null}
); }); -function DragHandle({ draggable, onDragStart, onDragEnd }: DragHandleProps): ReactElement { - const handleMouseDown = (e: MouseEvent): void => { - // Only stop propagation, don't prevent default - we need default for drag to work - e.stopPropagation(); - }; - - const handleClick = (e: MouseEvent): void => { - // Stop click events from bubbling to prevent sorting - e.stopPropagation(); - e.preventDefault(); - }; - - const handleDragStart = (e: DragEvent): void => { - // Don't stop propagation here - let the drag start properly - if (onDragStart) { - onDragStart(e); - } - }; - - const handleDragEnd = (e: DragEvent): void => { - if (onDragEnd) { - onDragEnd(e); - } - }; - - return ( - - â ¿ - - ); -} - -function SortIcon(): ReactNode { - const column = useColumn(); - switch (column.sortDir) { +function SortIcon({ direction }: SortIconProps): ReactNode { + switch (direction) { case "asc": return ; case "desc": diff --git a/packages/pluggableWidgets/datagrid-web/src/components/DragHandle.tsx b/packages/pluggableWidgets/datagrid-web/src/components/DragHandle.tsx new file mode 100644 index 0000000000..d988dc4b23 --- /dev/null +++ b/packages/pluggableWidgets/datagrid-web/src/components/DragHandle.tsx @@ -0,0 +1,46 @@ +import { DragEvent, DragEventHandler, MouseEvent, ReactElement } from "react"; +import { FaGripVertical } from "./icons/FaGripVertical"; + +interface DragHandleProps { + draggable: boolean; + onDragStart?: DragEventHandler; + onDragEnd?: DragEventHandler; +} +export function DragHandle({ draggable, onDragStart, onDragEnd }: DragHandleProps): ReactElement { + const handleMouseDown = (e: MouseEvent): void => { + // Only stop propagation, don't prevent default - we need default for drag to work + e.stopPropagation(); + }; + + const handleClick = (e: MouseEvent): void => { + // Stop click events from bubbling to prevent sorting + e.stopPropagation(); + e.preventDefault(); + }; + + const handleDragStart = (e: DragEvent): void => { + // Don't stop propagation here - let the drag start properly + if (onDragStart) { + onDragStart(e); + } + }; + + const handleDragEnd = (e: DragEvent): void => { + if (onDragEnd) { + onDragEnd(e); + } + }; + + return ( + + + + ); +} diff --git a/packages/pluggableWidgets/datagrid-web/src/components/icons/FaGripVertical.tsx b/packages/pluggableWidgets/datagrid-web/src/components/icons/FaGripVertical.tsx new file mode 100644 index 0000000000..b0e198d488 --- /dev/null +++ b/packages/pluggableWidgets/datagrid-web/src/components/icons/FaGripVertical.tsx @@ -0,0 +1,15 @@ +import { ReactElement } from "react"; + +/** + * Custom drag handle icon with 6 aligned dots in 3 rows + */ +export function FaGripVertical(): ReactElement { + return ( + + ); +} From 05a3d06a3a2f0b4decdf3c603a7bcd041f13c946 Mon Sep 17 00:00:00 2001 From: Yordan Stoyanov Date: Tue, 9 Dec 2025 14:12:53 +0100 Subject: [PATCH 16/19] chore: update unit test snapshot --- .../datagrid-web/e2e/DataGrid.spec.js | 28 ++++++++----------- .../ColumnContainer.spec.tsx.snap | 13 ++++++++- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/packages/pluggableWidgets/datagrid-web/e2e/DataGrid.spec.js b/packages/pluggableWidgets/datagrid-web/e2e/DataGrid.spec.js index 8a390bba23..5f301156b9 100644 --- a/packages/pluggableWidgets/datagrid-web/e2e/DataGrid.spec.js +++ b/packages/pluggableWidgets/datagrid-web/e2e/DataGrid.spec.js @@ -49,10 +49,9 @@ test.describe("capabilities: sorting", () => { await page.goto("/"); await page.waitForLoadState("networkidle"); await expect(page.locator(".mx-name-datagrid1 .column-header").nth(1)).toHaveText("First Name"); - await expect(page.locator(".mx-name-datagrid1 .column-header").nth(1).locator("svg")).toHaveAttribute( - "data-icon", - "arrows-alt-v" - ); + await expect( + page.locator(".mx-name-datagrid1 .column-header").nth(1).locator("svg[data-icon='arrows-alt-v']") + ).toBeVisible(); await expect(page.getByRole("gridcell", { name: "12" }).first()).toHaveText("12"); }); @@ -60,15 +59,13 @@ test.describe("capabilities: sorting", () => { await page.goto("/"); await page.waitForLoadState("networkidle"); await expect(page.locator(".mx-name-datagrid1 .column-header").nth(1)).toHaveText("First Name"); - await expect(page.locator(".mx-name-datagrid1 .column-header").nth(1).locator("svg")).toHaveAttribute( - "data-icon", - "arrows-alt-v" - ); + await expect( + page.locator(".mx-name-datagrid1 .column-header").nth(1).locator("svg[data-icon='arrows-alt-v']") + ).toBeVisible(); await page.locator(".mx-name-datagrid1 .column-header").nth(1).click(); - await expect(page.locator(".mx-name-datagrid1 .column-header").nth(1).locator("svg")).toHaveAttribute( - "data-icon", - "long-arrow-alt-up" - ); + await expect( + page.locator(".mx-name-datagrid1 .column-header").nth(1).locator("svg[data-icon='long-arrow-alt-up']") + ).toBeVisible(); await expect(page.getByRole("gridcell", { name: "10" }).first()).toHaveText("10"); }); @@ -78,10 +75,9 @@ test.describe("capabilities: sorting", () => { await expect(page.locator(".mx-name-datagrid1 .column-header").nth(1)).toHaveText("First Name"); await page.locator(".mx-name-datagrid1 .column-header").nth(1).click(); await page.locator(".mx-name-datagrid1 .column-header").nth(1).click(); - await expect(page.locator(".mx-name-datagrid1 .column-header").nth(1).locator("svg")).toHaveAttribute( - "data-icon", - "long-arrow-alt-down" - ); + await expect( + page.locator(".mx-name-datagrid1 .column-header").nth(1).locator("svg[data-icon='long-arrow-alt-down']") + ).toBeVisible(); await expect(page.getByRole("gridcell", { name: "12" }).first()).toHaveText("12"); }); }); diff --git a/packages/pluggableWidgets/datagrid-web/src/components/__tests__/__snapshots__/ColumnContainer.spec.tsx.snap b/packages/pluggableWidgets/datagrid-web/src/components/__tests__/__snapshots__/ColumnContainer.spec.tsx.snap index 1516a489f8..e836624349 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/__tests__/__snapshots__/ColumnContainer.spec.tsx.snap +++ b/packages/pluggableWidgets/datagrid-web/src/components/__tests__/__snapshots__/ColumnContainer.spec.tsx.snap @@ -52,7 +52,18 @@ exports[`ColumnContainer renders the structure correctly when draggable 1`] = ` class="drag-handle" draggable="true" > - â ¿ + Date: Thu, 11 Dec 2025 16:42:20 +0100 Subject: [PATCH 17/19] fix: restore individual column reorder and reflect in preview, revert null pass --- .../datagrid-web/src/Datagrid.editorPreview.tsx | 2 +- .../datagrid-web/src/components/ColumnContainer.tsx | 6 +++--- .../datagrid-web/src/model/containers/Datagrid.container.ts | 4 ++-- .../gallery-web/src/helpers/useItemSelectHelper.ts | 2 +- .../pluggableWidgets/gallery-web/src/utils/test-utils.tsx | 4 ++-- .../src/selection/select-action-handler.ts | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorPreview.tsx b/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorPreview.tsx index fcba79bf21..914ed2dcaa 100644 --- a/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorPreview.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorPreview.tsx @@ -174,7 +174,7 @@ function ColumnHeader({ column }: { column: ColumnsPreviewType }): ReactNode { >
- {columnsDraggable && ( + {columnsDraggable && columnPreview.canDrag && ( {}} onDragEnd={() => {}} /> )} {caption.length > 0 ? caption : "\u00a0"} diff --git a/packages/pluggableWidgets/datagrid-web/src/components/ColumnContainer.tsx b/packages/pluggableWidgets/datagrid-web/src/components/ColumnContainer.tsx index 979dcbe4d6..6e3744dc29 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/ColumnContainer.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/ColumnContainer.tsx @@ -30,9 +30,9 @@ export const ColumnContainer = observer(function ColumnContainer(props: ColumnCo style={!canSort ? { cursor: "unset" } : undefined} title={caption} data-column-id={columnId} - onDrop={vm.handleOnDrop} - onDragEnter={vm.handleDragEnter} - onDragOver={vm.handleDragOver} + onDrop={vm.isDraggable ? vm.handleOnDrop : undefined} + onDragEnter={vm.isDraggable ? vm.handleDragEnter : undefined} + onDragOver={vm.isDraggable ? vm.handleDragOver : 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 5524f296b3..4959148556 100644 --- a/packages/pluggableWidgets/datagrid-web/src/model/containers/Datagrid.container.ts +++ b/packages/pluggableWidgets/datagrid-web/src/model/containers/Datagrid.container.ts @@ -124,8 +124,8 @@ export class DatagridContainer extends Container { this.bind(CORE.columnsStore).toInstance(ColumnGroupStore).inSingletonScope(); // Drag and Drop store this.bind(DG.headerDragDrop).toInstance(HeaderDragnDropStore).inSingletonScope(); - // Drag and Drop view model - this.bind(DG.headerDragnDropVM).toInstance(HeaderDragnDropViewModel).inSingletonScope(); + // Drag and Drop view model (per column, not singleton) + this.bind(DG.headerDragnDropVM).toInstance(HeaderDragnDropViewModel).inTransientScope(); // Query service this.bind(DG.query).toInstance(DatasourceService).inSingletonScope(); // Grid sizing and scrolling store diff --git a/packages/pluggableWidgets/gallery-web/src/helpers/useItemSelectHelper.ts b/packages/pluggableWidgets/gallery-web/src/helpers/useItemSelectHelper.ts index 2e291dbca8..050128f4e0 100644 --- a/packages/pluggableWidgets/gallery-web/src/helpers/useItemSelectHelper.ts +++ b/packages/pluggableWidgets/gallery-web/src/helpers/useItemSelectHelper.ts @@ -6,5 +6,5 @@ export function useItemSelectHelper( selectionHelper: SelectionHelper | undefined ): SelectActionHandler { // eslint-disable-next-line react-hooks/exhaustive-deps - return useMemo(() => new SelectActionHandler(selection, selectionHelper ?? null), [selectionHelper]); + return useMemo(() => new SelectActionHandler(selection, selectionHelper), [selectionHelper]); } diff --git a/packages/pluggableWidgets/gallery-web/src/utils/test-utils.tsx b/packages/pluggableWidgets/gallery-web/src/utils/test-utils.tsx index 2caffacba8..0d5ac84aa3 100644 --- a/packages/pluggableWidgets/gallery-web/src/utils/test-utils.tsx +++ b/packages/pluggableWidgets/gallery-web/src/utils/test-utils.tsx @@ -81,7 +81,7 @@ export function createMockGalleryContext(): GalleryRootScope { refreshInterval: 0 }); - const mockSelectHelper = new SelectActionHandler("None", null); + const mockSelectHelper = new SelectActionHandler("None", undefined); return { rootStore: mockStore, @@ -110,7 +110,7 @@ type Mocks = { export function mockProps(params: Helpers & Mocks = {}): GalleryProps { const { onClick = undefined, - selectHelper = new SelectActionHandler("None", null), + selectHelper = new SelectActionHandler("None", undefined), actionHelper = new ClickActionHelper("single", onClick), focusController = new FocusTargetController(new PositionController(), new VirtualGridLayout(3, 4, 10)), itemEventsController = new ItemEventsController( 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 c5bcccad46..165f58d6cb 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 + protected selectionHelper: SelectionHelperService | undefined ) {} get selectionType(): SelectionType { From e083ab3402ec70dc77d23370238a5d8a8be48ed3 Mon Sep 17 00:00:00 2001 From: Yordan Stoyanov Date: Thu, 18 Dec 2025 13:37:51 +0100 Subject: [PATCH 18/19] feat: enhance drag-and-drop functionality with new drag handle and styling adjustments --- .../datawidgets/web/_datagrid.scss | 86 +++++++++++-------- .../datawidgets/web/variables.scss | 2 +- .../src/components/ColumnContainer.tsx | 4 + .../src/components/ColumnHeader.tsx | 4 - .../ColumnContainer.spec.tsx.snap | 34 ++++---- 5 files changed, 74 insertions(+), 56 deletions(-) diff --git a/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss b/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss index 850852725c..8ed0d62e70 100644 --- a/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss +++ b/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss @@ -22,7 +22,7 @@ $root: ".widget-datagrid"; background-color: var(--bg-color-secondary, $bg-color-secondary); border-width: 0; border-color: var(--grid-border-color, $grid-border-color); - padding: var(--spacing-medium, $spacing-medium); + padding: 0 var(--spacing-medium, $spacing-medium); top: 0; min-width: 0; position: relative; @@ -44,7 +44,7 @@ $root: ".widget-datagrid"; top: 0; height: 100%; width: var(--spacing-smaller, $spacing-smaller); - background-color: $dragging-color-effect; + background-color: var(--brand-primary, $dragging-color-effect); z-index: 1; } @@ -92,6 +92,55 @@ $root: ".widget-datagrid"; } } + /* Drag handle */ + .drag-handle { + cursor: grab; + pointer-events: auto; + position: relative; + padding: 4px; + // margin-inline-end: 4px; + flex-grow: 0; + flex-shrink: 0; + display: flex; + justify-content: center; + align-self: normal; + z-index: 1; + opacity: 0; + transition: opacity 0.15s ease; + + &:hover { + // background-color: var(--brand-primary-50, $brand-light); + svg { + color: var(--brand-primary, $brand-primary); + } + } + &:active { + cursor: grabbing; + } + &:focus-visible { + opacity: 1; + } + svg { + margin: 0; + width: 8px; + } + } + + &:hover .drag-handle, + &:focus-within .drag-handle { + opacity: 1; + } + + /* Parent background change on drag handle hover */ + &:has(.drag-handle:hover) { + background-color: var(--brand-primary-50, $brand-light); + } + + /* Remove left padding when drag handle is present */ + &:has(.drag-handle) { + padding-left: 0; + } + /* Content of the column header */ .column-container { display: flex; @@ -99,6 +148,7 @@ $root: ".widget-datagrid"; flex-grow: 1; align-self: stretch; min-width: 0; + padding: var(--spacing-small, $spacing-small) 0; &:not(:has(.filter)) { .column-header { @@ -131,38 +181,6 @@ $root: ".widget-datagrid"; align-self: center; } - /* Drag handle */ - .drag-handle { - cursor: grab; - pointer-events: auto; - position: relative; - width: 14px; - padding: 0; - flex-grow: 0; - flex-shrink: 0; - display: flex; - justify-content: center; - align-self: normal; - z-index: 1; - - &:hover { - background-color: var(--brand-primary-50, $brand-light); - svg { - color: var(--brand-primary, $brand-primary); - } - } - &:active { - cursor: grabbing; - } - svg { - margin: 0; - } - } - - .drag-handle + .column-caption { - padding-inline-start: 4px; - } - &:focus:not(:focus-visible) { outline: none; } diff --git a/packages/modules/data-widgets/src/themesource/datawidgets/web/variables.scss b/packages/modules/data-widgets/src/themesource/datawidgets/web/variables.scss index 8756e6e96b..7dafa4d574 100644 --- a/packages/modules/data-widgets/src/themesource/datawidgets/web/variables.scss +++ b/packages/modules/data-widgets/src/themesource/datawidgets/web/variables.scss @@ -33,7 +33,7 @@ $spacing-larger: 32px !default; $gallery-gap: $spacing-small !default; // Effects and animations -$dragging-color-effect: rgba(10, 19, 37, 0.8) !default; +$dragging-color-effect: $brand-primary !default; $skeleton-background: linear-gradient(90deg, rgba(194, 194, 194, 0.2) 0%, #d2d2d2 100%) !default; // Assets diff --git a/packages/pluggableWidgets/datagrid-web/src/components/ColumnContainer.tsx b/packages/pluggableWidgets/datagrid-web/src/components/ColumnContainer.tsx index 6e3744dc29..a6f0c233ef 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/ColumnContainer.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/ColumnContainer.tsx @@ -4,6 +4,7 @@ import { ColumnHeader } from "./ColumnHeader"; import { useColumn, useColumnsStore, useDatagridConfig, useHeaderDragnDropVM } from "../model/hooks/injection-hooks"; import { ColumnResizerProps } from "./ColumnResizer"; import { observer } from "mobx-react-lite"; +import { DragHandle } from "./DragHandle"; export interface ColumnContainerProps { isLast?: boolean; @@ -34,6 +35,9 @@ export const ColumnContainer = observer(function ColumnContainer(props: ColumnCo onDragEnter={vm.isDraggable ? vm.handleDragEnter : undefined} onDragOver={vm.isDraggable ? vm.handleDragOver : undefined} > + {vm.isDraggable && ( + + )}
{columnsFilterable && ( diff --git a/packages/pluggableWidgets/datagrid-web/src/components/ColumnHeader.tsx b/packages/pluggableWidgets/datagrid-web/src/components/ColumnHeader.tsx index 024ab37b12..b3632feff6 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/ColumnHeader.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/ColumnHeader.tsx @@ -1,6 +1,5 @@ import classNames from "classnames"; import { HTMLAttributes, KeyboardEvent, ReactElement, ReactNode } from "react"; -import { DragHandle } from "./DragHandle"; import { FaArrowsAltV } from "./icons/FaArrowsAltV"; import { FaLongArrowAltDown } from "./icons/FaLongArrowAltDown"; import { FaLongArrowAltUp } from "./icons/FaLongArrowAltUp"; @@ -26,9 +25,6 @@ export const ColumnHeader = observer(function ColumnHeader(): ReactElement { {...sortProps} aria-label={canSort ? "sort " + caption : caption} > - {vm.isDraggable && ( - - )} {caption.length > 0 ? caption : "\u00a0"} {canSort ? : null}
diff --git a/packages/pluggableWidgets/datagrid-web/src/components/__tests__/__snapshots__/ColumnContainer.spec.tsx.snap b/packages/pluggableWidgets/datagrid-web/src/components/__tests__/__snapshots__/ColumnContainer.spec.tsx.snap index e836624349..af030520b2 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/__tests__/__snapshots__/ColumnContainer.spec.tsx.snap +++ b/packages/pluggableWidgets/datagrid-web/src/components/__tests__/__snapshots__/ColumnContainer.spec.tsx.snap @@ -40,6 +40,23 @@ exports[`ColumnContainer renders the structure correctly when draggable 1`] = ` style="cursor: unset;" title="Column 1" > + + +
- - - From f027a908a555a0285fca132d2b6d986fe9701e08 Mon Sep 17 00:00:00 2001 From: Yordan Stoyanov Date: Tue, 23 Dec 2025 11:56:11 +0100 Subject: [PATCH 19/19] fix: adjust padding for improved layout in datagrid header and filter selector --- .../datawidgets/web/_datagrid.scss | 77 +++---- .../datawidgets/web/variables.scss | 1 + .../datagrid-web/CHANGELOG.md | 4 + .../datagrid-web/package.json | 3 + .../src/Datagrid.editorPreview.tsx | 6 +- .../src/components/ColumnContainer.tsx | 34 +-- .../src/components/ColumnHeader.tsx | 6 +- .../components/ColumnHeaderDragPreview.tsx | 40 ++++ .../src/components/DragHandle.tsx | 44 +--- .../src/components/DragHandleIcon.tsx | 15 ++ .../src/components/GridHeader.tsx | 71 ++++-- .../__tests__/ColumnContainer.spec.tsx | 71 ++---- .../ColumnContainer.spec.tsx.snap | 113 +++++++++- .../src/features/column/HeaderDnd.store.ts | 33 +++ .../features/column/HeaderDnd.viewModel.ts | 168 ++++++++++++++ .../features/column/HeaderDragnDrop.store.ts | 41 ---- .../column/HeaderDragnDrop.viewModel.ts | 95 -------- .../__tests__/HeaderDnd.viewModel.spec.ts | 59 +++++ .../HeaderDragnDrop.viewModel.spec.ts | 213 ------------------ .../src/helpers/state/ColumnGroupStore.ts | 54 ++++- .../model/containers/Datagrid.container.ts | 17 +- .../src/model/hooks/injection-hooks.ts | 2 +- .../datagrid-web/src/model/tokens.ts | 8 +- pnpm-lock.yaml | 56 +++++ 24 files changed, 687 insertions(+), 544 deletions(-) create mode 100644 packages/pluggableWidgets/datagrid-web/src/components/ColumnHeaderDragPreview.tsx create mode 100644 packages/pluggableWidgets/datagrid-web/src/components/DragHandleIcon.tsx create mode 100644 packages/pluggableWidgets/datagrid-web/src/features/column/HeaderDnd.store.ts create mode 100644 packages/pluggableWidgets/datagrid-web/src/features/column/HeaderDnd.viewModel.ts delete mode 100644 packages/pluggableWidgets/datagrid-web/src/features/column/HeaderDragnDrop.store.ts delete mode 100644 packages/pluggableWidgets/datagrid-web/src/features/column/HeaderDragnDrop.viewModel.ts create mode 100644 packages/pluggableWidgets/datagrid-web/src/features/column/__tests__/HeaderDnd.viewModel.spec.ts delete mode 100644 packages/pluggableWidgets/datagrid-web/src/features/column/__tests__/HeaderDragnDrop.viewModel.spec.ts diff --git a/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss b/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss index 8ed0d62e70..235bb5e3ed 100644 --- a/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss +++ b/packages/modules/data-widgets/src/themesource/datawidgets/web/_datagrid.scss @@ -22,7 +22,7 @@ $root: ".widget-datagrid"; background-color: var(--bg-color-secondary, $bg-color-secondary); border-width: 0; border-color: var(--grid-border-color, $grid-border-color); - padding: 0 var(--spacing-medium, $spacing-medium); + padding: calc(var(--spacing-small, $spacing-small) * 1.5) var(--spacing-medium, $spacing-medium); top: 0; min-width: 0; position: relative; @@ -30,41 +30,8 @@ $root: ".widget-datagrid"; } .th { - &.dragging { - opacity: 0.5; - &.dragging-over-self { - opacity: 0.8; - } - } - - &.drop-after:after, - &.drop-before:after { - content: ""; - position: absolute; - top: 0; - height: 100%; - width: var(--spacing-smaller, $spacing-smaller); - background-color: var(--brand-primary, $dragging-color-effect); - - z-index: 1; - } - - &.drop-before { - &:after { - left: 0; - } - &:not(:first-child):after { - transform: translateX(-50%); - } - } - - &.drop-after { - &:after { - right: 0; - } - &:not(:last-child):after { - transform: translateX(50%); - } + &.dragging-over-self { + opacity: 0.8; } /* Clickable column header (Sortable) */ @@ -78,6 +45,8 @@ $root: ".widget-datagrid"; align-self: stretch; cursor: col-resize; margin-right: -12px; + margin-top: calc(0px - var(--spacing-medium, 16px)); + margin-bottom: calc(0px - var(--spacing-medium, 16px)); &:hover .column-resizer-bar { background-color: var(--brand-primary, $brand-primary); @@ -97,19 +66,17 @@ $root: ".widget-datagrid"; cursor: grab; pointer-events: auto; position: relative; - padding: 4px; - // margin-inline-end: 4px; + padding: var(--spacing-smaller, $spacing-smaller); flex-grow: 0; flex-shrink: 0; display: flex; justify-content: center; - align-self: normal; + align-self: flex-start; z-index: 1; opacity: 0; transition: opacity 0.15s ease; &:hover { - // background-color: var(--brand-primary-50, $brand-light); svg { color: var(--brand-primary, $brand-primary); } @@ -126,6 +93,14 @@ $root: ".widget-datagrid"; } } + &.locked-drag-active { + z-index: 2; + } + + &.dragging-over-self { + opacity: 0.25; + } + &:hover .drag-handle, &:focus-within .drag-handle { opacity: 1; @@ -136,6 +111,21 @@ $root: ".widget-datagrid"; background-color: var(--brand-primary-50, $brand-light); } + /* Drag preview (dnd-kit) should look like hovered header */ + &.drag-preview { + background-color: var(--brand-primary-50, $brand-light); + box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.25); + border: 1px solid var(--gray-light, $gray-light); + + .drag-handle { + opacity: 1; + + svg { + color: var(--brand-primary, $brand-primary); + } + } + } + /* Remove left padding when drag handle is present */ &:has(.drag-handle) { padding-left: 0; @@ -148,7 +138,6 @@ $root: ".widget-datagrid"; flex-grow: 1; align-self: stretch; min-width: 0; - padding: var(--spacing-small, $spacing-small) 0; &:not(:has(.filter)) { .column-header { @@ -193,7 +182,9 @@ $root: ".widget-datagrid"; /* Header filter */ .filter { display: flex; - margin-top: 4px; + > * { + margin-top: 4px; + } > .form-group { margin-bottom: 0; } @@ -204,7 +195,7 @@ $root: ".widget-datagrid"; &:has(.th .column-container .filter:not(:empty)) { .th { &.column-selector { - padding: var(--spacing-medium, $spacing-medium) 0; + padding: calc(var(--spacing-small, $spacing-small) * 1.5) 0; } /*adjust filter-selector icon to be mid-bottom aligned */ .column-selector-content { diff --git a/packages/modules/data-widgets/src/themesource/datawidgets/web/variables.scss b/packages/modules/data-widgets/src/themesource/datawidgets/web/variables.scss index 7dafa4d574..1d2719721c 100644 --- a/packages/modules/data-widgets/src/themesource/datawidgets/web/variables.scss +++ b/packages/modules/data-widgets/src/themesource/datawidgets/web/variables.scss @@ -13,6 +13,7 @@ $brand-light: #e6eaff !default; $grid-selected-row-background: $brand-light !default; // Text and icon colors +$gray-light: #6c7180 !default; $gray-dark: #606671 !default; $gray-darker: #3b4251 !default; $pagination-caption-color: #0a1325 !default; diff --git a/packages/pluggableWidgets/datagrid-web/CHANGELOG.md b/packages/pluggableWidgets/datagrid-web/CHANGELOG.md index fa3c5989f1..1523a95d77 100644 --- a/packages/pluggableWidgets/datagrid-web/CHANGELOG.md +++ b/packages/pluggableWidgets/datagrid-web/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +### Breaking changes + +- The DOM structure is rewritten, which may break existing CSS styling. We recommend checking the custom styling if there is any in your project. + ### Fixed - We added missing Dutch translations for Datagrid 2. diff --git a/packages/pluggableWidgets/datagrid-web/package.json b/packages/pluggableWidgets/datagrid-web/package.json index 1d261d2c48..2d26fab4b1 100644 --- a/packages/pluggableWidgets/datagrid-web/package.json +++ b/packages/pluggableWidgets/datagrid-web/package.json @@ -41,6 +41,9 @@ "verify": "rui-verify-package-format" }, "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@floating-ui/react": "^0.26.27", "@mendix/widget-plugin-component-kit": "workspace:*", "@mendix/widget-plugin-external-events": "workspace:*", diff --git a/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorPreview.tsx b/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorPreview.tsx index 914ed2dcaa..810f56bb91 100644 --- a/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorPreview.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorPreview.tsx @@ -4,7 +4,6 @@ import { GUID, ObjectItem } from "mendix"; import { Selectable } from "mendix/preview/Selectable"; import { createContext, CSSProperties, PropsWithChildren, ReactElement, ReactNode, useContext } from "react"; import { ColumnsPreviewType, DatagridPreviewProps } from "typings/DatagridProps"; -import { DragHandle } from "./components/DragHandle"; import { FaArrowsAltV } from "./components/icons/FaArrowsAltV"; import { FaEye } from "./components/icons/FaEye"; import { ColumnPreview } from "./helpers/ColumnPreview"; @@ -159,7 +158,7 @@ function GridHeader(): ReactNode { } function ColumnHeader({ column }: { column: ColumnsPreviewType }): ReactNode { - const { columnsFilterable, columnsSortable, columnsHidable, columnsDraggable } = useProps(); + const { columnsFilterable, columnsSortable, columnsHidable } = useProps(); const columnPreview = new ColumnPreview(column, 0); const caption = columnPreview.header; const canSort = columnsSortable && columnPreview.canSort; @@ -174,9 +173,6 @@ function ColumnHeader({ column }: { column: ColumnsPreviewType }): ReactNode { >
- {columnsDraggable && columnPreview.canDrag && ( - {}} onDragEnd={() => {}} /> - )} {caption.length > 0 ? caption : "\u00a0"} {canSort && }
diff --git a/packages/pluggableWidgets/datagrid-web/src/components/ColumnContainer.tsx b/packages/pluggableWidgets/datagrid-web/src/components/ColumnContainer.tsx index a6f0c233ef..003ab1e258 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/ColumnContainer.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/ColumnContainer.tsx @@ -1,10 +1,11 @@ import classNames from "classnames"; import { ReactElement } from "react"; import { ColumnHeader } from "./ColumnHeader"; -import { useColumn, useColumnsStore, useDatagridConfig, useHeaderDragnDropVM } from "../model/hooks/injection-hooks"; +import { useColumn, useColumnsStore, useDatagridConfig, useHeaderDndVM } from "../model/hooks/injection-hooks"; import { ColumnResizerProps } from "./ColumnResizer"; import { observer } from "mobx-react-lite"; import { DragHandle } from "./DragHandle"; +import { useSortable } from "@dnd-kit/sortable"; export interface ColumnContainerProps { isLast?: boolean; @@ -12,36 +13,41 @@ export interface ColumnContainerProps { } export const ColumnContainer = observer(function ColumnContainer(props: ColumnContainerProps): ReactElement { - const { columnsFilterable, id: gridId } = useDatagridConfig(); - const { columnFilters } = useColumnsStore(); + const { columnsFilterable, columnsDraggable, id: gridId } = useDatagridConfig(); + const columnsStore = useColumnsStore(); + const { columnFilters } = columnsStore; const column = useColumn(); const { canSort, columnId, columnIndex, canResize, sortDir, header } = column; - const vm = useHeaderDragnDropVM(); const caption = header.trim(); + const vm = useHeaderDndVM(); + const isDndEnabled = Boolean(columnsDraggable && column.canDrag); + const { attributes, listeners, setNodeRef, setActivatorNodeRef, transform, transition, isDragging } = useSortable({ + id: columnId, + disabled: !isDndEnabled + }); + const style = vm.getHeaderCellStyle(columnId, { transform, transition }); + const isLocked = !column.canDrag; return (
- {vm.isDraggable && ( - + {isDndEnabled && ( + )}
{columnsFilterable && ( -
+
{columnFilters[columnIndex]?.renderFilterWidgets()}
)} diff --git a/packages/pluggableWidgets/datagrid-web/src/components/ColumnHeader.tsx b/packages/pluggableWidgets/datagrid-web/src/components/ColumnHeader.tsx index b3632feff6..7be4f1c0fc 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/ColumnHeader.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/ColumnHeader.tsx @@ -3,7 +3,7 @@ import { HTMLAttributes, KeyboardEvent, ReactElement, ReactNode } from "react"; import { FaArrowsAltV } from "./icons/FaArrowsAltV"; import { FaLongArrowAltDown } from "./icons/FaLongArrowAltDown"; import { FaLongArrowAltUp } from "./icons/FaLongArrowAltUp"; -import { useColumn, useHeaderDragnDropVM } from "../model/hooks/injection-hooks"; +import { useColumn, useHeaderDndVM } from "../model/hooks/injection-hooks"; import { observer } from "mobx-react-lite"; import { SortDirection } from "../typings/sorting"; @@ -16,12 +16,12 @@ export const ColumnHeader = observer(function ColumnHeader(): ReactElement { const { header, canSort, alignment } = column; const caption = header.trim(); const sortProps = canSort ? getSortProps(() => column.toggleSort()) : null; - const vm = useHeaderDragnDropVM(); + const vm = useHeaderDndVM(); return (
diff --git a/packages/pluggableWidgets/datagrid-web/src/components/ColumnHeaderDragPreview.tsx b/packages/pluggableWidgets/datagrid-web/src/components/ColumnHeaderDragPreview.tsx new file mode 100644 index 0000000000..1b0b2cb046 --- /dev/null +++ b/packages/pluggableWidgets/datagrid-web/src/components/ColumnHeaderDragPreview.tsx @@ -0,0 +1,40 @@ +import classNames from "classnames"; +import { ReactElement } from "react"; +import { useColumn, useColumnsStore, useDatagridConfig } from "../model/hooks/injection-hooks"; +import { ColumnHeader } from "./ColumnHeader"; +import { DragHandleIcon } from "./DragHandleIcon"; + +/** + * Drag preview content for column header reordering. + * + * Rendered by @dnd-kit DragOverlay in a portal, so we provide the same selector context + * used by the datagrid SCSS to make it look like a real header. + */ +export function ColumnHeaderDragPreview(): ReactElement { + const { columnsFilterable, id: gridId } = useDatagridConfig(); + const { columnFilters } = useColumnsStore(); + const column = useColumn(); + const { columnId, columnIndex, header, size } = column; + const caption = header.trim(); + + return ( +
+
+
+ +
+ + {columnsFilterable && ( +
{columnFilters[columnIndex]?.renderFilterWidgets()}
+ )} +
+
+
+
+ ); +} diff --git a/packages/pluggableWidgets/datagrid-web/src/components/DragHandle.tsx b/packages/pluggableWidgets/datagrid-web/src/components/DragHandle.tsx index d988dc4b23..4293774f81 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/DragHandle.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/DragHandle.tsx @@ -1,45 +1,15 @@ -import { DragEvent, DragEventHandler, MouseEvent, ReactElement } from "react"; +import { ReactElement } from "react"; +import { DraggableAttributes, DraggableSyntheticListeners } from "@dnd-kit/core"; import { FaGripVertical } from "./icons/FaGripVertical"; interface DragHandleProps { - draggable: boolean; - onDragStart?: DragEventHandler; - onDragEnd?: DragEventHandler; + setActivatorNodeRef: (element: HTMLElement | null) => void; + listeners?: DraggableSyntheticListeners; + attributes?: DraggableAttributes; } -export function DragHandle({ draggable, onDragStart, onDragEnd }: DragHandleProps): ReactElement { - const handleMouseDown = (e: MouseEvent): void => { - // Only stop propagation, don't prevent default - we need default for drag to work - e.stopPropagation(); - }; - - const handleClick = (e: MouseEvent): void => { - // Stop click events from bubbling to prevent sorting - e.stopPropagation(); - e.preventDefault(); - }; - - const handleDragStart = (e: DragEvent): void => { - // Don't stop propagation here - let the drag start properly - if (onDragStart) { - onDragStart(e); - } - }; - - const handleDragEnd = (e: DragEvent): void => { - if (onDragEnd) { - onDragEnd(e); - } - }; - +export function DragHandle({ setActivatorNodeRef, listeners, attributes }: DragHandleProps): ReactElement { return ( - + ); diff --git a/packages/pluggableWidgets/datagrid-web/src/components/DragHandleIcon.tsx b/packages/pluggableWidgets/datagrid-web/src/components/DragHandleIcon.tsx new file mode 100644 index 0000000000..160eff94f4 --- /dev/null +++ b/packages/pluggableWidgets/datagrid-web/src/components/DragHandleIcon.tsx @@ -0,0 +1,15 @@ +import { ReactElement } from "react"; +import { FaGripVertical } from "./icons/FaGripVertical"; + +/** + * Visual-only drag handle. + * + * For preview purposes only; does not implement drag-and-drop functionality. + */ +export function DragHandleIcon(): ReactElement { + return ( + + ); +} diff --git a/packages/pluggableWidgets/datagrid-web/src/components/GridHeader.tsx b/packages/pluggableWidgets/datagrid-web/src/components/GridHeader.tsx index cab4ef45f2..ffd5dd10d8 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/GridHeader.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/GridHeader.tsx @@ -1,40 +1,69 @@ -import { ReactElement } from "react"; -import { useColumnsStore, useDatagridConfig, useGridSizeStore } from "../model/hooks/injection-hooks"; +import { ReactElement, useMemo } from "react"; +import { useColumnsStore, useDatagridConfig, useGridSizeStore, useHeaderDndVM } from "../model/hooks/injection-hooks"; import { CheckboxColumnHeader } from "./CheckboxColumnHeader"; import { ColumnProvider } from "./ColumnProvider"; import { ColumnResizer } from "./ColumnResizer"; import { ColumnSelector } from "./ColumnSelector"; import { ColumnContainer } from "./ColumnContainer"; import { HeaderSkeletonLoader } from "./loader/HeaderSkeletonLoader"; +import { observer } from "mobx-react-lite"; +import { DndContext, DragOverlay, KeyboardSensor, PointerSensor, useSensor, useSensors } from "@dnd-kit/core"; +import { horizontalListSortingStrategy, SortableContext, sortableKeyboardCoordinates } from "@dnd-kit/sortable"; +import { ColumnHeaderDragPreview } from "./ColumnHeaderDragPreview"; -export function GridHeader(): ReactElement { +export const GridHeader = observer(function GridHeader(): ReactElement { const { columnsHidable, id: gridId } = useDatagridConfig(); const columnsStore = useColumnsStore(); const gridSizeStore = useGridSizeStore(); const columns = columnsStore.visibleColumns; + const vm = useHeaderDndVM(); + const items = useMemo(() => columns.filter(c => c.canDrag).map(c => c.columnId), [columns]); + const sensors = useSensors( + useSensor(PointerSensor, { activationConstraint: { distance: 5 } }), + useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }) + ); if (!columnsStore.loaded) { return ; } return ( -
-
- - {columns.map(column => ( - - } /> + + +
+
+ + {columns.map(column => ( + + } /> + + ))} + {columnsHidable && ( + + )} +
+
+
+ + + {vm.activeColumn ? ( + + - ))} - {columnsHidable && ( - - )} -
-
+ ) : null} + + ); -} +}); diff --git a/packages/pluggableWidgets/datagrid-web/src/components/__tests__/ColumnContainer.spec.tsx b/packages/pluggableWidgets/datagrid-web/src/components/__tests__/ColumnContainer.spec.tsx index 63167f0fe9..f179c0f9a5 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/__tests__/ColumnContainer.spec.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/__tests__/ColumnContainer.spec.tsx @@ -3,14 +3,29 @@ import "@testing-library/jest-dom"; import { render } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { ContainerProvider } from "brandi-react"; +import { ReactElement } from "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 { ColumnContainer } from "../ColumnContainer"; import { ColumnResizer } from "../ColumnResizer"; +import { DndContext } from "@dnd-kit/core"; +import { horizontalListSortingStrategy, SortableContext } from "@dnd-kit/sortable"; describe("ColumnContainer", () => { + function renderWithProviders(container: any, col: any, element: ReactElement): ReturnType { + return render( + + + + {element} + + + + ); + } + it("renders the structure correctly", () => { const props = mockContainerProps({ columns: [column("Column 1")] @@ -19,13 +34,7 @@ describe("ColumnContainer", () => { const columns = container.get(CORE_TOKENS.columnsStore); const col = columns.visibleColumns[0]; - const component = render( - - - } /> - - - ); + const component = renderWithProviders(container, col, } />); expect(component.asFragment()).toMatchSnapshot(); }); @@ -41,13 +50,7 @@ describe("ColumnContainer", () => { const columns = container.get(CORE_TOKENS.columnsStore); const col = columns.visibleColumns[0]; - const component = render( - - - } /> - - - ); + const component = renderWithProviders(container, col, } />); expect(component.asFragment()).toMatchSnapshot(); }); @@ -63,13 +66,7 @@ describe("ColumnContainer", () => { const columns = container.get(CORE_TOKENS.columnsStore); const col = columns.visibleColumns[0]; - const component = render( - - - resizer
} /> - - - ); + const component = renderWithProviders(container, col, resizer
} />); expect(component.asFragment()).toMatchSnapshot(); }); @@ -85,13 +82,7 @@ describe("ColumnContainer", () => { const columns = container.get(CORE_TOKENS.columnsStore); const col = columns.visibleColumns[0]; - const component = render( - - - } /> - - - ); + const component = renderWithProviders(container, col, } />); expect(component.asFragment()).toMatchSnapshot(); }); @@ -107,13 +98,7 @@ describe("ColumnContainer", () => { const columns = container.get(CORE_TOKENS.columnsStore); const col = columns.visibleColumns[0]; - const component = render( - - - } /> - - - ); + const component = renderWithProviders(container, col, } />); expect(component.asFragment()).toMatchSnapshot(); }); @@ -131,13 +116,7 @@ describe("ColumnContainer", () => { const col = columns.visibleColumns[0]; const spy = jest.spyOn(col, "toggleSort"); - const component = render( - - - } /> - - - ); + const component = renderWithProviders(container, col, } />); const button = component.getByLabelText("sort Column 1"); expect(button).toBeInTheDocument(); @@ -156,13 +135,7 @@ describe("ColumnContainer", () => { const columns = container.get(CORE_TOKENS.columnsStore); const col = columns.visibleColumns[0]; - const component = render( - - - } /> - - - ); + const component = renderWithProviders(container, col, } />); expect(component.asFragment()).toMatchSnapshot(); }); }); diff --git a/packages/pluggableWidgets/datagrid-web/src/components/__tests__/__snapshots__/ColumnContainer.spec.tsx.snap b/packages/pluggableWidgets/datagrid-web/src/components/__tests__/__snapshots__/ColumnContainer.spec.tsx.snap index af030520b2..4f7e5b2eb9 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/__tests__/__snapshots__/ColumnContainer.spec.tsx.snap +++ b/packages/pluggableWidgets/datagrid-web/src/components/__tests__/__snapshots__/ColumnContainer.spec.tsx.snap @@ -6,7 +6,6 @@ exports[`ColumnContainer renders the structure correctly 1`] = ` class="th" data-column-id="0" role="columnheader" - style="cursor: unset;" title="Column 1" >
+ +
`; @@ -37,12 +53,15 @@ exports[`ColumnContainer renders the structure correctly when draggable 1`] = ` class="th" data-column-id="0" role="columnheader" - style="cursor: unset;" title="Column 1" >
+ +
`; @@ -85,7 +121,6 @@ exports[`ColumnContainer renders the structure correctly when filterable with cu class="th" data-column-id="0" role="columnheader" - style="cursor: unset;" title="Column 1" >
+ +
`; @@ -120,7 +172,6 @@ exports[`ColumnContainer renders the structure correctly when resizable 1`] = ` class="th" data-column-id="0" role="columnheader" - style="cursor: unset;" title="Column 1" >
+ +
`; @@ -190,6 +258,23 @@ exports[`ColumnContainer renders the structure correctly when sortable 1`] = ` />
+ +
`; @@ -199,7 +284,6 @@ exports[`ColumnContainer renders the structure correctly when value is empty 1`] class="th" data-column-id="0" role="columnheader" - style="cursor: unset;" title="" >
+ +
`; diff --git a/packages/pluggableWidgets/datagrid-web/src/features/column/HeaderDnd.store.ts b/packages/pluggableWidgets/datagrid-web/src/features/column/HeaderDnd.store.ts new file mode 100644 index 0000000000..9efe3794e0 --- /dev/null +++ b/packages/pluggableWidgets/datagrid-web/src/features/column/HeaderDnd.store.ts @@ -0,0 +1,33 @@ +import { makeAutoObservable } from "mobx"; +import { ColumnId } from "../../typings/GridColumn"; + +export type DropPlacement = "before" | "after"; + +export class HeaderDndStore { + activeId: ColumnId | undefined = undefined; + overId: ColumnId | undefined = undefined; + placement: DropPlacement | undefined = undefined; + + constructor() { + makeAutoObservable(this); + } + + get isDragging(): boolean { + return Boolean(this.activeId); + } + + setActive(id: ColumnId | undefined): void { + this.activeId = id; + } + + setOver(overId: ColumnId | undefined, placement: DropPlacement | undefined): void { + this.overId = overId; + this.placement = placement; + } + + clear(): void { + this.activeId = undefined; + this.overId = undefined; + this.placement = undefined; + } +} diff --git a/packages/pluggableWidgets/datagrid-web/src/features/column/HeaderDnd.viewModel.ts b/packages/pluggableWidgets/datagrid-web/src/features/column/HeaderDnd.viewModel.ts new file mode 100644 index 0000000000..45036e8621 --- /dev/null +++ b/packages/pluggableWidgets/datagrid-web/src/features/column/HeaderDnd.viewModel.ts @@ -0,0 +1,168 @@ +import { makeAutoObservable } from "mobx"; +import { + closestCenter, + CollisionDetection, + DragCancelEvent, + DragEndEvent, + DragOverEvent, + DragStartEvent, + UniqueIdentifier +} from "@dnd-kit/core"; +import { CSS, Transform } from "@dnd-kit/utilities"; +import { CSSProperties } from "react"; +import { ColumnId, GridColumn } from "../../typings/GridColumn"; +import { ColumnGroupStore } from "../../helpers/state/ColumnGroupStore"; +import { DropPlacement, HeaderDndStore } from "./HeaderDnd.store"; + +export class HeaderDndViewModel { + constructor( + private store: HeaderDndStore, + private columnsStore: ColumnGroupStore + ) { + makeAutoObservable(this, {}, { autoBind: true }); + } + + get activeId(): ColumnId | undefined { + return this.store.activeId; + } + + get overId(): ColumnId | undefined { + return this.store.overId; + } + + get placement(): DropPlacement | undefined { + return this.store.placement; + } + + get isDragging(): boolean { + return this.store.isDragging; + } + + get activeColumn(): GridColumn | undefined { + const activeId = this.store.activeId; + if (!activeId) { + return undefined; + } + return this.columnsStore.availableColumns.find(c => c.columnId === activeId); + } + + /** + * dnd-kit collision detection implementation. + * + * Kept here so barrier rules live with the DnD state/logic, not the view. + */ + collisionDetection(args: Parameters[0]): ReturnType { + // Use the full set of droppable containers for collision detection so + // dnd-kit's sorting/animation calculations consider locked columns' positions. + // We still prevent final drops on locked columns in `onDragOver`/`onDragEnd`. + return closestCenter(args); + } + + /** + * Derive the inline style for a header cell based on sortable state. + * Only draggable columns receive transform/transition styles to avoid + * showing locked columns moving during a drag. + */ + getHeaderCellStyle( + columnId: ColumnId, + options: { transform: Transform | null; transition: string | undefined } + ): CSSProperties { + const columns = this.columnsStore.visibleColumns; + const idx = columns.findIndex(c => c.columnId === columnId); + // If column doesn't exist or is locked (non-draggable) + // do not apply transform/transition so it remains visually fixed during dnd. + if (idx < 0 || !columns[idx].canDrag) { + return {}; + } + + return { + ...(options.transform ? { transform: CSS.Transform.toString(options.transform) } : null), + ...(options.transition ? { transition: options.transition } : null) + }; + } + + /** + * Ids that are allowed to be used as "over" targets for the current active drag. + * Returns undefined when not currently dragging. + */ + private toColumnId(id: UniqueIdentifier | undefined): ColumnId | undefined { + return typeof id === "string" ? (id as ColumnId) : undefined; + } + + private computePlacement(active: ColumnId, over: ColumnId): DropPlacement | undefined { + // Compute placement based on the indices of movable columns only so + // locked (non-draggable / non-sortable) columns remain stationary during drags. + const columns = this.columnsStore.visibleColumns; + const movable = columns.filter(c => c.canDrag); + const activeIndex = movable.findIndex(c => c.columnId === active); + const overIndex = movable.findIndex(c => c.columnId === over); + if (activeIndex < 0 || overIndex < 0) { + return undefined; + } + return overIndex > activeIndex ? "after" : "before"; + } + + private isOverAllowed(over: ColumnId): boolean { + const columns = this.columnsStore.visibleColumns; + const overIndex = columns.findIndex(c => c.columnId === over); + if (overIndex < 0) { + return false; + } + + // Can't drop onto a non-draggable column. + const col = columns[overIndex]; + return Boolean(col.canDrag); + } + + onDragStart(e: DragStartEvent): void { + const activeId = this.toColumnId(e.active.id); + if (activeId) { + this.store.setActive(activeId); + } + } + + onDragOver(e: DragOverEvent): void { + const activeId = this.toColumnId(e.active.id); + const overId = this.toColumnId(e.over?.id); + + if (!activeId) { + this.store.setOver(undefined, undefined); + return; + } + + if (!overId || activeId === overId) { + this.store.setOver(undefined, undefined); + return; + } + + if (!this.isOverAllowed(overId)) { + this.store.setOver(undefined, undefined); + return; + } + + this.store.setOver(overId, this.computePlacement(activeId, overId)); + } + + onDragEnd(e: DragEndEvent): void { + const activeId = this.toColumnId(e.active.id); + const overId = this.toColumnId(e.over?.id); + + if (!activeId) { + this.store.clear(); + return; + } + + if (overId && activeId !== overId && this.isOverAllowed(overId)) { + const placement = this.computePlacement(activeId, overId); + if (placement) { + this.columnsStore.swapColumns(activeId, [overId, placement]); + } + } + + this.store.clear(); + } + + onDragCancel(_e: DragCancelEvent): void { + this.store.clear(); + } +} diff --git a/packages/pluggableWidgets/datagrid-web/src/features/column/HeaderDragnDrop.store.ts b/packages/pluggableWidgets/datagrid-web/src/features/column/HeaderDragnDrop.store.ts deleted file mode 100644 index 54fc13b982..0000000000 --- a/packages/pluggableWidgets/datagrid-web/src/features/column/HeaderDragnDrop.store.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { action, makeAutoObservable } from "mobx"; -import { ColumnId } from "../../typings/GridColumn"; - -/** - * MobX store for managing drag & drop state of column headers. - * Tracks which column is being dragged and where it can be dropped. - * @injectable - */ -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/features/column/HeaderDragnDrop.viewModel.ts b/packages/pluggableWidgets/datagrid-web/src/features/column/HeaderDragnDrop.viewModel.ts deleted file mode 100644 index fed47eca29..0000000000 --- a/packages/pluggableWidgets/datagrid-web/src/features/column/HeaderDragnDrop.viewModel.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { makeAutoObservable } from "mobx"; -import { DragEvent } from "react"; -import { HeaderDragnDropStore } from "./HeaderDragnDrop.store"; -import { ColumnId, GridColumn } 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. - * @injectable - */ -export class HeaderDragnDropViewModel { - constructor( - private dndStore: HeaderDragnDropStore, - private columnsStore: ColumnGroupStore, - private config: { columnsDraggable: boolean }, - private column: GridColumn - ) { - makeAutoObservable(this); - } - - get dropTarget(): [ColumnId, "before" | "after"] | undefined { - return this.dndStore.dragOver; - } - - get dragging(): [ColumnId | undefined, ColumnId, ColumnId | undefined] | undefined { - return this.dndStore.isDragging; - } - - get isDraggable(): boolean { - return this.config.columnsDraggable && this.column.canDrag; - } - - 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]); - }; - - 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]); - } - }; - - handleDragEnter = (e: DragEvent): void => { - e.preventDefault(); - }; - - handleDragEnd = (): void => { - this.dndStore.clearDragState(); - }; - - 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/__tests__/HeaderDnd.viewModel.spec.ts b/packages/pluggableWidgets/datagrid-web/src/features/column/__tests__/HeaderDnd.viewModel.spec.ts new file mode 100644 index 0000000000..df3486ed35 --- /dev/null +++ b/packages/pluggableWidgets/datagrid-web/src/features/column/__tests__/HeaderDnd.viewModel.spec.ts @@ -0,0 +1,59 @@ +import { HeaderDndStore } from "../HeaderDnd.store"; +import { HeaderDndViewModel } from "../HeaderDnd.viewModel"; +import { ColumnId } from "../../../typings/GridColumn"; + +function col(id: string, canDrag: boolean): any { + return { columnId: id as ColumnId, canDrag }; +} + +describe("HeaderDndViewModel", () => { + it("does not swap across a non-draggable column barrier", () => { + const store = new HeaderDndStore(); + const columnsStore: any = { + visibleColumns: [col("A", true), col("B", false), col("C", true), col("D", true)], + availableColumns: [col("A", true), col("B", false), col("C", true), col("D", true)], + swapColumns: jest.fn() + }; + + const vm = new HeaderDndViewModel(store, columnsStore); + + // Try to drag C (right side) over A (left side). With the updated + // behavior draggable columns may swap across non-draggable columns. + vm.onDragEnd({ active: { id: "C" }, over: { id: "A" } } as any); + + expect(columnsStore.swapColumns).toHaveBeenCalledTimes(1); + expect(columnsStore.swapColumns).toHaveBeenCalledWith("C", ["A", "before"]); + }); + + it("allows swapping within the same draggable segment", () => { + const store = new HeaderDndStore(); + const columnsStore: any = { + visibleColumns: [col("A", true), col("B", false), col("C", true), col("D", true)], + availableColumns: [col("A", true), col("B", false), col("C", true), col("D", true)], + swapColumns: jest.fn() + }; + + const vm = new HeaderDndViewModel(store, columnsStore); + + // Drag D over C (same segment). Should reorder. + vm.onDragEnd({ active: { id: "D" }, over: { id: "C" } } as any); + + expect(columnsStore.swapColumns).toHaveBeenCalledTimes(1); + expect(columnsStore.swapColumns).toHaveBeenCalledWith("D", ["C", "before"]); + }); + + it("does not allow dropping onto a non-draggable column", () => { + const store = new HeaderDndStore(); + const columnsStore: any = { + visibleColumns: [col("A", true), col("B", false), col("C", true)], + availableColumns: [col("A", true), col("B", false), col("C", true)], + swapColumns: jest.fn() + }; + + const vm = new HeaderDndViewModel(store, columnsStore); + + vm.onDragEnd({ active: { id: "C" }, over: { id: "B" } } as any); + + expect(columnsStore.swapColumns).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/pluggableWidgets/datagrid-web/src/features/column/__tests__/HeaderDragnDrop.viewModel.spec.ts b/packages/pluggableWidgets/datagrid-web/src/features/column/__tests__/HeaderDragnDrop.viewModel.spec.ts deleted file mode 100644 index a11aa24123..0000000000 --- a/packages/pluggableWidgets/datagrid-web/src/features/column/__tests__/HeaderDragnDrop.viewModel.spec.ts +++ /dev/null @@ -1,213 +0,0 @@ -import { DragEvent } from "react"; -import { HeaderDragnDropViewModel } from "../HeaderDragnDrop.viewModel"; -import { HeaderDragnDropStore } from "../HeaderDragnDrop.store"; -import { ColumnId } from "../../../typings/GridColumn"; - -describe("ColumnHeaderViewModel", () => { - let dndStore: HeaderDragnDropStore; - let mockColumnsStore: any; - let mockColumn: any; - - beforeEach(() => { - dndStore = new HeaderDragnDropStore(); - mockColumnsStore = { - swapColumns: jest.fn() - }; - mockColumn = { - canDrag: true, - columnId: "col1" as ColumnId - }; - }); - - describe("when columnsDraggable is false", () => { - it("is not draggable", () => { - const vm = new HeaderDragnDropViewModel( - dndStore, - mockColumnsStore, - { columnsDraggable: false }, - mockColumn - ); - - expect(vm.isDraggable).toBe(false); - }); - }); - - describe("when columnsDraggable is true", () => { - let vm: HeaderDragnDropViewModel; - - beforeEach(() => { - vm = new HeaderDragnDropViewModel(dndStore, mockColumnsStore, { columnsDraggable: true }, mockColumn); - }); - - it("is draggable", () => { - expect(vm.isDraggable).toBe(true); - }); - - describe("handleDragStart", () => { - it("sets dragging state with column siblings", () => { - const mockElement = createMockElement("col1", "col0", "col2"); - const event = createMockDragEvent(mockElement); - - vm.handleDragStart(event); - - expect(dndStore.isDragging).toEqual(["col0", "col1", "col2"]); - }); - - it("handles missing previous sibling", () => { - const mockElement = createMockElement("col1", undefined, "col2"); - const event = createMockDragEvent(mockElement); - - vm.handleDragStart(event); - - expect(dndStore.isDragging).toEqual([undefined, "col1", "col2"]); - }); - - it("handles missing next sibling", () => { - const mockElement = createMockElement("col1", "col0", undefined); - const event = createMockDragEvent(mockElement); - - vm.handleDragStart(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.handleDragStart(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.handleDragOver(event); - - expect(dndStore.dragOver).toBeUndefined(); - }); - - it("does nothing when columnId is missing", () => { - const event = createMockDragOverEvent("", 100, 50); - - vm.handleDragOver(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.handleDragOver(event); - - expect(dndStore.dragOver).toBeUndefined(); - }); - - it("sets dropTarget to before when hovering over left sibling", () => { - const event = createMockDragOverEvent("col0", 100, 50); - - vm.handleDragOver(event); - - expect(dndStore.dragOver).toEqual(["col0", "before"]); - }); - - it("sets dropTarget to after when hovering over right sibling", () => { - const event = createMockDragOverEvent("col2", 100, 50); - - vm.handleDragOver(event); - - expect(dndStore.dragOver).toEqual(["col2", "after"]); - }); - }); - - describe("handleDragEnter", () => { - it("prevents default behavior", () => { - const event = { preventDefault: jest.fn() } as any; - - vm.handleDragEnter(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.handleDragEnd(); - - 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.handleOnDrop({} 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.handleOnDrop({} 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; -} diff --git a/packages/pluggableWidgets/datagrid-web/src/helpers/state/ColumnGroupStore.ts b/packages/pluggableWidgets/datagrid-web/src/helpers/state/ColumnGroupStore.ts index e8cf112bd2..7288debf7c 100644 --- a/packages/pluggableWidgets/datagrid-web/src/helpers/state/ColumnGroupStore.ts +++ b/packages/pluggableWidgets/datagrid-web/src/helpers/state/ColumnGroupStore.ts @@ -127,10 +127,58 @@ export class ColumnGroupStore implements IColumnGroupStore, IColumnParentStore, swapColumns(source: ColumnId, [target, placement]: [ColumnId, "after" | "before"]): void { const columnSource = this._allColumnsById.get(source)!; const columnTarget = this._allColumnsById.get(target)!; - columnSource.orderWeight = columnTarget.orderWeight + (placement === "after" ? 1 : -1); - // normalize columns - this._allColumnsOrdered.forEach((column, idx) => { + // Reorder only among movable (draggable) columns so that locked + // (non-draggable) columns keep their original positions. + const allOrdered = this._allColumnsOrdered; + const movable = allOrdered.filter(c => c.canDrag); + + const srcMovIdx = movable.findIndex(c => c.columnId === source); + const tgtMovIdx = movable.findIndex(c => c.columnId === target); + + // Fallback to simple relative weight if either column is not in movable set + if (srcMovIdx < 0 || tgtMovIdx < 0) { + columnSource.orderWeight = columnTarget.orderWeight + (placement === "after" ? 1 : -1); + this._allColumnsOrdered.forEach((column, idx) => { + column.orderWeight = idx * 10; + }); + return; + } + + // Build new movable order with source removed and inserted at target position + const newMovable = movable.slice(); + // remove source + newMovable.splice(srcMovIdx, 1); + + // compute insert index relative to original movable indices + const originalTgtIdx = tgtMovIdx; + let insertIdx: number; + if (srcMovIdx < originalTgtIdx) { + // removing source shifted target left by 1 + insertIdx = originalTgtIdx - 1 + (placement === "after" ? 1 : 0); + } else { + insertIdx = originalTgtIdx + (placement === "after" ? 1 : 0); + } + + // clamp + if (insertIdx < 0) insertIdx = 0; + if (insertIdx > newMovable.length) insertIdx = newMovable.length; + + newMovable.splice(insertIdx, 0, columnSource); + + // Merge back into the full ordered list: replace movable slots in-order + const merged: ColumnStore[] = []; + let movableCursor = 0; + for (const col of allOrdered) { + if (col.canDrag) { + merged.push(newMovable[movableCursor++]); + } else { + merged.push(col); + } + } + + // normalize weights based on merged order + merged.forEach((column, idx) => { column.orderWeight = idx * 10; }); } 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 4959148556..73a54b915f 100644 --- a/packages/pluggableWidgets/datagrid-web/src/model/containers/Datagrid.container.ts +++ b/packages/pluggableWidgets/datagrid-web/src/model/containers/Datagrid.container.ts @@ -31,8 +31,8 @@ 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 { HeaderDragnDropViewModel } from "../../features/column/HeaderDragnDrop.viewModel"; +import { HeaderDndStore } from "../../features/column/HeaderDnd.store"; +import { HeaderDndViewModel } from "../../features/column/HeaderDnd.viewModel"; import { GridBasicData } from "../../helpers/state/GridBasicData"; import { GridPersonalizationStore } from "../../helpers/state/GridPersonalizationStore"; import { DatagridConfig } from "../configs/Datagrid.config"; @@ -108,9 +108,9 @@ injected( DG.selectionCounterCfg.optional ); -// drag and drop -injected(HeaderDragnDropStore); -injected(HeaderDragnDropViewModel, DG.headerDragDrop, CORE.columnsStore, CORE.config, CORE.column); +// header drag and drop (dnd-kit) +injected(HeaderDndStore); +injected(HeaderDndViewModel, DG.headerDndStore, CORE.columnsStore); export class DatagridContainer extends Container { id = `DatagridContainer@${generateUUID()}`; @@ -122,10 +122,9 @@ 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(); - // Drag and Drop view model (per column, not singleton) - this.bind(DG.headerDragnDropVM).toInstance(HeaderDragnDropViewModel).inTransientScope(); + // Header drag and drop (dnd-kit) store/view model + this.bind(DG.headerDndStore).toInstance(HeaderDndStore).inSingletonScope(); + this.bind(DG.headerDndVM).toInstance(HeaderDndViewModel).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 8a5ae2451e..6cd9f1d9f2 100644 --- a/packages/pluggableWidgets/datagrid-web/src/model/hooks/injection-hooks.ts +++ b/packages/pluggableWidgets/datagrid-web/src/model/hooks/injection-hooks.ts @@ -23,7 +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 [useHeaderDragnDropVM] = createInjectionHooks(DG.headerDragnDropVM); +export const [useHeaderDndVM] = createInjectionHooks(DG.headerDndVM); 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 1f45c3455e..d0e8fbb29a 100644 --- a/packages/pluggableWidgets/datagrid-web/src/model/tokens.ts +++ b/packages/pluggableWidgets/datagrid-web/src/model/tokens.ts @@ -38,8 +38,8 @@ 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 { HeaderDragnDropViewModel } from "../features/column/HeaderDragnDrop.viewModel"; +import { HeaderDndStore } from "../features/column/HeaderDnd.store"; +import { HeaderDndViewModel } from "../features/column/HeaderDnd.viewModel"; import { DatasourceParamsController } from "../model/services/DatasourceParamsController"; import { GridColumn } from "../typings/GridColumn"; import { DatagridConfig } from "./configs/Datagrid.config"; @@ -148,8 +148,8 @@ export const DG_TOKENS = { clickActionHelper: token("@service:ClickActionHelper"), focusService: token("@service:FocusTargetController"), checkboxEventsHandler: token("@service:CheckboxEventsController"), - headerDragDrop: token("@store:HeaderDragnDropStore"), - headerDragnDropVM: token("@viewmodel:ColumnHeaderViewModel"), + headerDndStore: token("@store:HeaderDndStore"), + headerDndVM: token("@viewmodel:HeaderDndViewModel"), cellEventsHandler: token("@service:CellEventsController") }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b5e929788a..7bf3bf8b89 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1275,6 +1275,15 @@ importers: packages/pluggableWidgets/datagrid-web: dependencies: + '@dnd-kit/core': + specifier: ^6.3.1 + version: 6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@dnd-kit/sortable': + specifier: ^10.0.0 + version: 10.0.0(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + '@dnd-kit/utilities': + specifier: ^3.2.2 + version: 3.2.2(react@18.3.1) '@floating-ui/react': specifier: ^0.26.27 version: 0.26.28(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -3861,6 +3870,28 @@ packages: resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} engines: {node: '>=10.0.0'} + '@dnd-kit/accessibility@3.1.1': + resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==} + peerDependencies: + react: '>=18.0.0 <19.0.0' + + '@dnd-kit/core@6.3.1': + resolution: {integrity: sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==} + peerDependencies: + react: '>=18.0.0 <19.0.0' + react-dom: '>=18.0.0 <19.0.0' + + '@dnd-kit/sortable@10.0.0': + resolution: {integrity: sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==} + peerDependencies: + '@dnd-kit/core': ^6.3.0 + react: '>=18.0.0 <19.0.0' + + '@dnd-kit/utilities@3.2.2': + resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==} + peerDependencies: + react: '>=18.0.0 <19.0.0' + '@eslint-community/eslint-utils@4.9.0': resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -11939,6 +11970,31 @@ snapshots: '@discoveryjs/json-ext@0.5.7': {} + '@dnd-kit/accessibility@3.1.1(react@18.3.1)': + dependencies: + react: 18.3.1 + tslib: 2.8.1 + + '@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@dnd-kit/accessibility': 3.1.1(react@18.3.1) + '@dnd-kit/utilities': 3.2.2(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + tslib: 2.8.1 + + '@dnd-kit/sortable@10.0.0(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': + dependencies: + '@dnd-kit/core': 6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@dnd-kit/utilities': 3.2.2(react@18.3.1) + react: 18.3.1 + tslib: 2.8.1 + + '@dnd-kit/utilities@3.2.2(react@18.3.1)': + dependencies: + react: 18.3.1 + tslib: 2.8.1 + '@eslint-community/eslint-utils@4.9.0(eslint@7.32.0)': dependencies: eslint: 7.32.0