diff --git a/package.json b/package.json index efccd1e5fc..f9cc2c134d 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "@codemirror/state": "^6.5.2", "@codemirror/view": "^6.38.1", "@mendix/pluggable-widgets-tools": "10.21.2", + "@testing-library/react": ">=15.0.6", "@types/big.js": "^6.2.2", "@types/node": "~22.14.0", "@types/react": ">=18.2.36", diff --git a/packages/pluggableWidgets/datagrid-web/src/Datagrid.tsx b/packages/pluggableWidgets/datagrid-web/src/Datagrid.tsx index f6f3a80d50..3eeddd6be1 100644 --- a/packages/pluggableWidgets/datagrid-web/src/Datagrid.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/Datagrid.tsx @@ -1,142 +1,22 @@ -import { useClickActionHelper } from "@mendix/widget-plugin-grid/helpers/ClickActionHelper"; -import { useFocusTargetController } from "@mendix/widget-plugin-grid/keyboard-navigation/useFocusTargetController"; -import { useSelectionHelper } from "@mendix/widget-plugin-grid/selection"; -import { useConst } from "@mendix/widget-plugin-mobx-kit/react/useConst"; -import { generateUUID } from "@mendix/widget-plugin-platform/framework/generate-uuid"; import { ContainerProvider } from "brandi-react"; import { observer } from "mobx-react-lite"; -import { ReactElement, ReactNode, useCallback, useMemo } from "react"; +import { ReactElement } from "react"; import { DatagridContainerProps } from "../typings/DatagridProps"; -import { Cell } from "./components/Cell"; import { Widget } from "./components/Widget"; -import { WidgetHeaderContext } from "./components/WidgetHeaderContext"; import { useDataExport } from "./features/data-export/useDataExport"; -import { useCellEventsController } from "./features/row-interaction/CellEventsController"; -import { useCheckboxEventsController } from "./features/row-interaction/CheckboxEventsController"; -import { LegacyContext } from "./helpers/root-context"; -import { useSelectActionHelper } from "./helpers/SelectActionHelper"; import { useDataGridJSActions } from "./helpers/useDataGridJSActions"; -import { - useColumnsStore, - useExportProgressService, - useLoaderViewModel, - useMainGate, - usePaginationService -} from "./model/hooks/injection-hooks"; +import { useColumnsStore, useExportProgressService } from "./model/hooks/injection-hooks"; import { useDatagridContainer } from "./model/hooks/useDatagridContainer"; const DatagridRoot = observer((props: DatagridContainerProps): ReactElement => { - const gate = useMainGate(); const columnsStore = useColumnsStore(); - const paginationService = usePaginationService(); const exportProgress = useExportProgressService(); - const loaderVM = useLoaderViewModel(); - const items = gate.props.datasource.items ?? []; const [abortExport] = useDataExport(props, columnsStore, exportProgress); - const selectionHelper = useSelectionHelper( - gate.props.itemSelection, - gate.props.datasource, - props.onSelectionChange, - props.keepSelection ? "always keep" : "always clear" - ); - - const selectActionHelper = useSelectActionHelper(props, selectionHelper); - - const clickActionHelper = useClickActionHelper({ - onClickTrigger: props.onClickTrigger, - onClick: props.onClick - }); - - useDataGridJSActions(selectActionHelper); - - const visibleColumnsCount = selectActionHelper.showCheckboxColumn - ? columnsStore.visibleColumns.length + 1 - : columnsStore.visibleColumns.length; + useDataGridJSActions(); - const focusController = useFocusTargetController({ - rows: items.length, - columns: visibleColumnsCount, - pageSize: props.pageSize - }); - - const cellEventsController = useCellEventsController(selectActionHelper, clickActionHelper, focusController); - - const checkboxEventsController = useCheckboxEventsController(selectActionHelper, focusController); - - return ( - - ReactElement) => - props.showEmptyPlaceholder === "custom" ? renderWrapper(props.emptyPlaceholder) :
, - [props.emptyPlaceholder, props.showEmptyPlaceholder] - )} - filterRenderer={useCallback( - (renderWrapper, columnIndex) => { - const columnFilter = columnsStore.columnFilters[columnIndex]; - return renderWrapper(columnFilter.renderFilterWidgets()); - }, - [columnsStore.columnFilters] - )} - headerTitle={props.filterSectionTitle?.value} - headerContent={ - props.filtersPlaceholder && ( - - {props.filtersPlaceholder} - - ) - } - hasMoreItems={props.datasource.hasMoreItems ?? false} - headerWrapperRenderer={useCallback((_columnIndex: number, header: ReactElement) => header, [])} - id={useMemo(() => `DataGrid${generateUUID()}`, [])} - numberOfItems={props.datasource.totalCount} - onExportCancel={abortExport} - page={paginationService.currentPage} - pageSize={props.pageSize} - paginationType={props.pagination} - loadMoreButtonCaption={props.loadMoreButtonCaption?.value} - paging={paginationService.showPagination} - pagingPosition={props.pagingPosition} - showPagingButtons={props.showPagingButtons} - rowClass={useCallback((value: any) => props.rowClass?.get(value)?.value ?? "", [props.rowClass])} - setPage={paginationService.setPage} - styles={props.style} - exporting={exportProgress.inProgress} - processedRows={exportProgress.loaded} - visibleColumns={columnsStore.visibleColumns} - availableColumns={columnsStore.availableColumns} - setIsResizing={(status: boolean) => columnsStore.setIsResizing(status)} - columnsSwap={(moved, [target, placement]) => columnsStore.swapColumns(moved, [target, placement])} - selectActionHelper={selectActionHelper} - cellEventsController={cellEventsController} - checkboxEventsController={checkboxEventsController} - focusController={focusController} - isFirstLoad={loaderVM.isFirstLoad} - isFetchingNextBatch={loaderVM.isFetchingNextBatch} - showRefreshIndicator={loaderVM.showRefreshIndicator} - loadingType={props.loadingType} - columnsLoading={!columnsStore.loaded} - /> - - ); + return ; }); DatagridRoot.displayName = "DatagridComponent"; diff --git a/packages/pluggableWidgets/datagrid-web/src/components/CheckboxCell.tsx b/packages/pluggableWidgets/datagrid-web/src/components/CheckboxCell.tsx index 8c67978e5b..c907a62820 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/CheckboxCell.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/CheckboxCell.tsx @@ -1,8 +1,12 @@ import { useFocusTargetProps } from "@mendix/widget-plugin-grid/keyboard-navigation/useFocusTargetProps"; import { ObjectItem } from "mendix"; -import { FocusEvent, ReactElement } from "react"; -import { useLegacyContext } from "../helpers/root-context"; -import { useBasicData } from "../model/hooks/injection-hooks"; +import { FocusEvent, ReactElement, useMemo } from "react"; +import { + useCheckboxEventsHandler, + useDatagridConfig, + useSelectActions, + useTexts +} from "../model/hooks/injection-hooks"; import { CellElement, CellElementProps } from "./CellElement"; export type CheckboxCellProps = CellElementProps & { @@ -12,17 +16,19 @@ export type CheckboxCellProps = CellElementProps & { }; export function CheckboxCell({ item, rowIndex, lastRow, ...rest }: CheckboxCellProps): ReactElement { + const config = useDatagridConfig(); + const selectActions = useSelectActions(); + const checkboxEventsHandler = useCheckboxEventsHandler(); + const { selectRowLabel } = useTexts(); const keyNavProps = useFocusTargetProps({ columnIndex: 0, rowIndex }); - const { selectActionHelper, checkboxEventsController } = useLegacyContext(); - const { selectRowLabel, gridInteractive } = useBasicData(); return ( - + checkboxEventsHandler.getProps(item), [item, checkboxEventsHandler])} /> ); diff --git a/packages/pluggableWidgets/datagrid-web/src/components/CheckboxColumnHeader.tsx b/packages/pluggableWidgets/datagrid-web/src/components/CheckboxColumnHeader.tsx index dec1876094..810c64fd3e 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/CheckboxColumnHeader.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/CheckboxColumnHeader.tsx @@ -1,41 +1,38 @@ +import { If } from "@mendix/widget-plugin-component-kit/If"; import { ThreeStateCheckBox } from "@mendix/widget-plugin-component-kit/ThreeStateCheckBox"; -import { SelectionStatus } from "@mendix/widget-plugin-grid/selection"; +import { observer } from "mobx-react-lite"; import { Fragment, ReactElement, ReactNode } from "react"; -import { useLegacyContext } from "../helpers/root-context"; -import { useBasicData } from "../model/hooks/injection-hooks"; +import { useDatagridConfig, useSelectActions, useSelectionHelper, useTexts } from "../model/hooks/injection-hooks"; export function CheckboxColumnHeader(): ReactElement { - const { selectActionHelper, selectionHelper } = useLegacyContext(); - const { showCheckboxColumn, showSelectAllToggle, onSelectAll } = selectActionHelper; - const { selectAllRowsLabel } = useBasicData(); + const { selectAllCheckboxEnabled, checkboxColumnEnabled } = useDatagridConfig(); - if (showCheckboxColumn === false) { + if (checkboxColumnEnabled === false) { return ; } return (
- {showSelectAllToggle && ( - - )} + + +
); } -function Checkbox(props: { status: SelectionStatus; onChange: () => void; "aria-label"?: string }): ReactNode { - if (props.status === "unknown") { - console.error("Data grid 2: don't know how to render column checkbox with selectionStatus=unknown"); +const Checkbox = observer(function Checkbox(): ReactNode { + const { selectAllRowsLabel } = useTexts(); + const selectionHelper = useSelectionHelper(); + const selectActions = useSelectActions(); + + if (!selectionHelper || selectionHelper.type !== "Multi") { return null; } return ( selectActions.selectPage()} + aria-label={selectAllRowsLabel || "Select all rows"} /> ); -} +}); diff --git a/packages/pluggableWidgets/datagrid-web/src/components/ColumnProvider.tsx b/packages/pluggableWidgets/datagrid-web/src/components/ColumnProvider.tsx new file mode 100644 index 0000000000..cc4003d69b --- /dev/null +++ b/packages/pluggableWidgets/datagrid-web/src/components/ColumnProvider.tsx @@ -0,0 +1,17 @@ +import { useConst } from "@mendix/widget-plugin-mobx-kit/react/useConst"; +import { Container } from "brandi"; +import { ContainerProvider } from "brandi-react"; +import { PropsWithChildren, ReactNode } from "react"; +import { CORE_TOKENS as CORE } from "../model/tokens"; +import { GridColumn } from "../typings/GridColumn"; + +/** Provider to bind & provider column store for children at runtime. */ +export function ColumnProvider(props: PropsWithChildren<{ column: GridColumn }>): ReactNode { + const ct = useConst(() => { + const container = new Container(); + container.bind(CORE.column).toConstant(props.column); + return container; + }); + + return {props.children}; +} diff --git a/packages/pluggableWidgets/datagrid-web/src/components/Cell.tsx b/packages/pluggableWidgets/datagrid-web/src/components/DataCell.tsx similarity index 73% rename from packages/pluggableWidgets/datagrid-web/src/components/Cell.tsx rename to packages/pluggableWidgets/datagrid-web/src/components/DataCell.tsx index 653862e79a..70307640f6 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/Cell.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/DataCell.tsx @@ -1,12 +1,26 @@ import { useFocusTargetProps } from "@mendix/widget-plugin-grid/keyboard-navigation/useFocusTargetProps"; +import { ObjectItem } from "mendix"; import { computed } from "mobx"; import { observer } from "mobx-react-lite"; -import { ReactElement, useMemo } from "react"; -import { CellComponentProps } from "../typings/CellComponent"; +import { ReactElement, ReactNode, useMemo } from "react"; +import { EventsController } from "../typings/CellComponent"; import { GridColumn } from "../typings/GridColumn"; import { CellElement } from "./CellElement"; -const component = observer(function Cell(props: CellComponentProps): ReactElement { +interface DataCellProps { + children?: ReactNode; + className?: string; + column: GridColumn; + item: ObjectItem; + key?: string | number; + rowIndex: number; + columnIndex?: number; + clickable?: boolean; + preview?: boolean; + eventsController: EventsController; +} + +export const DataCell = observer(function DataCell(props: DataCellProps): ReactElement { const keyNavProps = useFocusTargetProps({ columnIndex: props.columnIndex ?? -1, rowIndex: props.rowIndex @@ -36,6 +50,3 @@ const component = observer(function Cell(props: CellComponentProps):
); }); - -// Override NamedExoticComponent type -export const Cell = component as (props: CellComponentProps) => ReactElement; diff --git a/packages/pluggableWidgets/datagrid-web/src/components/ExportWidget.tsx b/packages/pluggableWidgets/datagrid-web/src/components/ExportWidget.tsx deleted file mode 100644 index 7ab41f5b7f..0000000000 --- a/packages/pluggableWidgets/datagrid-web/src/components/ExportWidget.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { PropsWithChildren, ReactElement } from "react"; -import { PseudoModal } from "./PseudoModal"; -import { ExportAlert, ExportAlertProps } from "./ExportAlert"; - -type ExportWidgetProps = PropsWithChildren< - ExportAlertProps & { - open: boolean; - } ->; - -export function ExportWidget({ open, ...alertProps }: ExportWidgetProps): ReactElement | null { - if (!open) { - return null; - } - - return ( - - - - ); -} diff --git a/packages/pluggableWidgets/datagrid-web/src/components/Grid.tsx b/packages/pluggableWidgets/datagrid-web/src/components/Grid.tsx index 96948a3bca..015fb3f539 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/Grid.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/Grid.tsx @@ -1,18 +1,18 @@ -import classNames from "classnames"; -import { ComponentPropsWithoutRef, ReactElement } from "react"; - -type P = Omit, "role">; - -export interface GridProps extends P { - className?: string; -} - -export function Grid(props: GridProps): ReactElement { - const { className, style, children, ...rest } = props; +import { observer } from "mobx-react-lite"; +import { PropsWithChildren, ReactElement } from "react"; +import { useDatagridConfig, useGridStyle } from "../model/hooks/injection-hooks"; +export const Grid = observer(function Grid(props: PropsWithChildren): ReactElement { + const config = useDatagridConfig(); + const style = useGridStyle().get(); return ( -
- {children} +
+ {props.children}
); -} +}); diff --git a/packages/pluggableWidgets/datagrid-web/src/components/GridBody.tsx b/packages/pluggableWidgets/datagrid-web/src/components/GridBody.tsx index b9c3e76bb1..00d054f82d 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/GridBody.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/GridBody.tsx @@ -1,87 +1,107 @@ +import { useInfiniteControl } from "@mendix/widget-plugin-grid/components/InfiniteBody"; import classNames from "classnames"; -import { Fragment, ReactElement, ReactNode } from "react"; -import { LoadingTypeEnum, PaginationEnum } from "../../typings/DatagridProps"; -import { SpinnerLoader } from "./loader/SpinnerLoader"; +import { observer } from "mobx-react-lite"; +import { Fragment, PropsWithChildren, ReactElement, ReactNode, RefObject, UIEventHandler, useCallback } from "react"; +import { + useDatagridConfig, + useItemCount, + useLoaderViewModel, + usePaginationService, + useVisibleColumnsCount +} from "../model/hooks/injection-hooks"; import { RowSkeletonLoader } from "./loader/RowSkeletonLoader"; -import { useInfiniteControl } from "@mendix/widget-plugin-grid/components/InfiniteBody"; - -interface Props { - className?: string; - children?: ReactNode; - loadingType: LoadingTypeEnum; - isFirstLoad: boolean; - isFetchingNextBatch?: boolean; - columnsHidable: boolean; - columnsSize: number; - rowsSize: number; - pageSize: number; - pagination: PaginationEnum; - hasMoreItems: boolean; - setPage?: (update: (page: number) => number) => void; -} - -export function GridBody(props: Props): ReactElement { - const { children, pagination, hasMoreItems, setPage } = props; - - const isInfinite = pagination === "virtualScrolling"; - const [trackScrolling, bodySize, containerRef] = useInfiniteControl({ - hasMoreItems, - isInfinite, - setPage - }); +import { SpinnerLoader } from "./loader/SpinnerLoader"; - const content = (): ReactElement => { - if (props.isFirstLoad) { - return 0 ? props.rowsSize : props.pageSize} />; - } - return ( - - {children} - {props.isFetchingNextBatch && } - - ); - }; +export const GridBody = observer(function GridBody(props: PropsWithChildren): ReactElement { + const { children } = props; + const { bodySize, containerRef, isInfinite, handleScroll } = useBodyScroll(); return (
0 ? { maxHeight: `${bodySize}px` } : {}} role="rowgroup" ref={containerRef} - onScroll={isInfinite ? trackScrolling : undefined} + onScroll={handleScroll} > - {content()} + {children}
); -} +}); -interface LoaderProps { - loadingType: LoadingTypeEnum; - columnsHidable: boolean; - columnsSize: number; - rowsSize: number; - useBorderTop?: boolean; -} +const ContentGuard = observer(function ContentGuard(props: PropsWithChildren): ReactNode { + const loaderVM = useLoaderViewModel(); + const { pageSize } = usePaginationService(); + const config = useDatagridConfig(); + const columnsCount = useVisibleColumnsCount().get(); + const itemCount = useItemCount().get(); + + if (loaderVM.isFirstLoad && config.loadingType === "spinner") { + return ; + } -function Loader(props: LoaderProps): ReactElement { - if (props.loadingType === "spinner") { + if (loaderVM.isFirstLoad) { return ( -
- -
+ 0 ? itemCount : pageSize} + useBorderTop + /> ); } return ( - + + {props.children} + {(() => { + if (loaderVM.isFetchingNextBatch && config.loadingType === "spinner") { + return ; + } + + if (loaderVM.isFetchingNextBatch) { + return ( + + ); + } + + return null; + })()} + ); +}); + +function useBodyScroll(): { + handleScroll: UIEventHandler | undefined; + bodySize: number; + containerRef: RefObject; + isInfinite: boolean; +} { + const paging = usePaginationService(); + const setPage = useCallback((cb: (n: number) => number) => paging.setPage(cb), [paging]); + + const isInfinite = paging.pagination === "virtualScrolling"; + const [trackScrolling, bodySize, containerRef] = useInfiniteControl({ + hasMoreItems: paging.hasMoreItems, + isInfinite, + setPage + }); + + return { + handleScroll: isInfinite ? trackScrolling : undefined, + bodySize, + containerRef, + isInfinite + }; } + +const Spinner = (): ReactNode => ( +
+ +
+); diff --git a/packages/pluggableWidgets/datagrid-web/src/components/GridHeader.tsx b/packages/pluggableWidgets/datagrid-web/src/components/GridHeader.tsx index 5ef8785352..39b86c6d3a 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/GridHeader.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/GridHeader.tsx @@ -1,57 +1,21 @@ -import { ReactElement, ReactNode, useCallback, useState } from "react"; -import { ColumnId, GridColumn } from "../typings/GridColumn"; +import { ReactElement, useState } from "react"; +import { useColumnsStore, useDatagridConfig } 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 { HeaderSkeletonLoader } from "./loader/HeaderSkeletonLoader"; -type GridHeaderProps = { - availableColumns: GridColumn[]; - columns: GridColumn[]; - setIsResizing: (status: boolean) => void; - columnsDraggable: boolean; - columnsFilterable: boolean; - columnsHidable: boolean; - columnsResizable: boolean; - columnsSortable: boolean; - columnsSwap: (source: ColumnId, target: [ColumnId, "after" | "before"]) => void; - filterRenderer: (renderWrapper: (children: ReactNode) => ReactElement, columnIndex: number) => ReactElement; - headerWrapperRenderer: (columnIndex: number, header: ReactElement) => ReactElement; - id: string; - isLoading: boolean; - preview?: boolean; -}; - -export function GridHeader({ - availableColumns, - columns, - setIsResizing, - columnsDraggable, - columnsFilterable, - columnsHidable, - columnsResizable, - columnsSortable, - columnsSwap, - filterRenderer, - headerWrapperRenderer, - id, - isLoading, - preview -}: GridHeaderProps): ReactElement { +export function GridHeader(): ReactElement { + const { columnsHidable, id: gridId } = useDatagridConfig(); + const columnsStore = useColumnsStore(); + const columns = columnsStore.visibleColumns; const [dragOver, setDragOver] = useState<[ColumnId, "before" | "after"] | undefined>(undefined); const [isDragging, setIsDragging] = useState<[ColumnId | undefined, ColumnId, ColumnId | undefined] | undefined>(); - const renderFilterWrapper = useCallback( - (children: ReactNode) => ( -
- {children} -
- ), - [isDragging] - ); - - if (isLoading) { + if (!columnsStore.loaded) { return ; } @@ -59,41 +23,28 @@ export function GridHeader({
- {columns.map((column, index) => - headerWrapperRenderer( - index, + {columns.map(column => ( +
setIsResizing(true)} - onResizeEnds={() => setIsResizing(false)} + onResizeStart={() => columnsStore.setIsResizing(true)} + onResizeEnds={() => columnsStore.setIsResizing(false)} setColumnWidth={(width: number) => column.setSize(width)} /> } - swapColumns={columnsSwap} setDropTarget={setDragOver} setIsDragging={setIsDragging} - sortable={columnsSortable} /> - ) - )} + + ))} {columnsHidable && ( )} diff --git a/packages/pluggableWidgets/datagrid-web/src/components/Header.tsx b/packages/pluggableWidgets/datagrid-web/src/components/Header.tsx index 54c35f8c6a..9e7b7beec6 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/Header.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/Header.tsx @@ -1,3 +1,4 @@ +import classNames from "classnames"; import { Dispatch, DragEvent, @@ -9,82 +10,69 @@ import { SetStateAction, useCallback } from "react"; -import classNames from "classnames"; +import { FaArrowsAltV } from "./icons/FaArrowsAltV"; import { FaLongArrowAltDown } from "./icons/FaLongArrowAltDown"; import { FaLongArrowAltUp } from "./icons/FaLongArrowAltUp"; -import { FaArrowsAltV } from "./icons/FaArrowsAltV"; -import { ColumnResizerProps } from "./ColumnResizer"; +import { useColumn, useColumnsStore, useDatagridConfig } from "../model/hooks/injection-hooks"; import { ColumnId, GridColumn } from "../typings/GridColumn"; +import { ColumnResizerProps } from "./ColumnResizer"; export interface HeaderProps { - className?: string; - gridId: string; - column: GridColumn; - sortable: boolean; - resizable: boolean; - filterable: boolean; - hidable: boolean; - draggable: boolean; - filterWidget?: ReactNode; - preview?: boolean; + isLast?: boolean; resizer: ReactElement; + dropTarget?: [ColumnId, "before" | "after"]; isDragging?: [ColumnId | undefined, ColumnId, ColumnId | undefined]; setDropTarget: Dispatch>; setIsDragging: Dispatch>; - swapColumns: (source: ColumnId, target: [ColumnId, "before" | "after"]) => void; } export function Header(props: HeaderProps): ReactElement { - const canSort = props.sortable && props.column.canSort; - const canDrag = props.draggable && (props.column.canDrag ?? false); + const { columnsFilterable, id: gridId } = useDatagridConfig(); + const columnsStore = useColumnsStore(); + const column = useColumn(); + const { canDrag, canSort } = column; + const draggableProps = useDraggable( canDrag, - props.swapColumns, + columnsStore.swapColumns.bind(columnsStore), props.dropTarget, props.setDropTarget, props.isDragging, props.setIsDragging ); - const sortIcon = canSort ? getSortIcon(props.column) : null; - const sortProps = canSort ? getSortProps(props.column) : null; - const caption = props.column.header.trim(); + const sortIcon = canSort ? getSortIcon(column) : null; + const sortProps = canSort ? getSortProps(column) : null; + const caption = column.header.trim(); return (
props.column.setHeaderElementRef(ref)} - data-column-id={props.column.columnId} + ref={ref => column.setHeaderElementRef(ref)} + data-column-id={column.columnId} onDrop={draggableProps.onDrop} onDragEnter={draggableProps.onDragEnter} onDragOver={draggableProps.onDragOver} >
{caption.length > 0 ? caption : "\u00a0"} {sortIcon}
- {props.filterable && props.filterWidget} + {columnsFilterable && ( +
+ {columnsStore.columnFilters[column.columnIndex]?.renderFilterWidgets()} +
+ )}
- {props.resizable && props.column.canResize && props.resizer} + {column.canResize ? props.resizer : null}
); } diff --git a/packages/pluggableWidgets/datagrid-web/src/components/Pagination.tsx b/packages/pluggableWidgets/datagrid-web/src/components/Pagination.tsx new file mode 100644 index 0000000000..fb8200c6d8 --- /dev/null +++ b/packages/pluggableWidgets/datagrid-web/src/components/Pagination.tsx @@ -0,0 +1,25 @@ +import { Pagination as PaginationComponent } from "@mendix/widget-plugin-grid/components/Pagination"; +import { observer } from "mobx-react-lite"; +import { ReactNode } from "react"; +import { usePaginationService } from "../model/hooks/injection-hooks"; + +export const Pagination = observer(function Pagination(): ReactNode { + const paging = usePaginationService(); + + if (!paging.paginationVisible) return null; + + return ( + paging.setPage(page)} + nextPage={() => paging.setPage(n => n + 1)} + numberOfItems={paging.totalCount} + page={paging.currentPage} + pageSize={paging.pageSize} + showPagingButtons={paging.showPagingButtons} + previousPage={() => paging.setPage(n => n - 1)} + pagination={paging.pagination} + /> + ); +}); diff --git a/packages/pluggableWidgets/datagrid-web/src/components/RefreshStatus.tsx b/packages/pluggableWidgets/datagrid-web/src/components/RefreshStatus.tsx new file mode 100644 index 0000000000..aaf0446990 --- /dev/null +++ b/packages/pluggableWidgets/datagrid-web/src/components/RefreshStatus.tsx @@ -0,0 +1,11 @@ +import { RefreshIndicator } from "@mendix/widget-plugin-component-kit/RefreshIndicator"; +import { ReactNode } from "react"; +import { useLoaderViewModel } from "../model/hooks/injection-hooks"; + +export const RefreshStatus = function RefreshStatus(): ReactNode { + const loaderVM = useLoaderViewModel(); + + if (!loaderVM.showRefreshIndicator) return null; + + return loaderVM.isRefreshing ? : null; +}; diff --git a/packages/pluggableWidgets/datagrid-web/src/components/Row.tsx b/packages/pluggableWidgets/datagrid-web/src/components/Row.tsx index c89ed119d4..4d41de7428 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/Row.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/Row.tsx @@ -1,40 +1,39 @@ +import { SelectActionsService } from "@mendix/widget-plugin-grid/main"; import classNames from "classnames"; import { ObjectItem } from "mendix"; import { ReactElement } from "react"; -import { CellComponent, EventsController } from "../typings/CellComponent"; +import { EventsController } from "../typings/CellComponent"; import { GridColumn } from "../typings/GridColumn"; -import { SelectorCell } from "./SelectorCell"; import { CheckboxCell } from "./CheckboxCell"; -import { SelectActionHelper } from "../helpers/SelectActionHelper"; +import { DataCell } from "./DataCell"; +import { SelectorCell } from "./SelectorCell"; -export interface RowProps { +export interface RowProps { className?: string; - CellComponent: CellComponent; - columns: C[]; + columns: GridColumn[]; item: ObjectItem; index: number; showSelectorCell?: boolean; - selectableWrapper?: (column: number, children: ReactElement) => ReactElement; - selectActionHelper: SelectActionHelper; - preview: boolean; + selectActions: SelectActionsService; totalRows: number; clickable: boolean; eventsController: EventsController; + checkboxColumnEnabled: boolean; } -export function Row(props: RowProps): ReactElement { - const { CellComponent: Cell, selectActionHelper, preview, totalRows, eventsController } = props; - const selected = selectActionHelper.isSelected(props.item); - const ariaSelected = selectActionHelper.selectionType === "None" ? undefined : selected; +export function Row(props: RowProps): ReactElement { + const { selectActions, totalRows, eventsController } = props; + const selected = selectActions.isSelected(props.item); + const ariaSelected = selectActions.selectionType === "None" ? undefined : selected; const borderTop = props.index === 0; return (
- {selectActionHelper.showCheckboxColumn && ( + {props.checkboxColumnEnabled && ( (props: RowProps): ReactElement { /> )} {props.columns.map((column, baseIndex) => { - const cell = ( - ); - - return preview ? props.selectableWrapper?.(baseIndex, cell) : cell; })} {props.showSelectorCell && ( ; - columns: GridColumn[]; - columnsHidable: boolean; - eventsController: EventsController; - focusController: FocusTargetController; - interactive: boolean; - pageSize: number; - preview: boolean; - rowClass?: (item: ObjectItem) => string; - rows: ObjectItem[]; - selectableWrapper?: (column: number, children: ReactElement) => ReactElement; - selectActionHelper: SelectActionHelper; -} +export const RowsRenderer = observer(function RowsRenderer(): ReactElement { + const rows = useRows().get(); + const config = useDatagridConfig(); + const { visibleColumns } = useColumnsStore(); + const rowClass = useRowClass(); + const cellEventsController = useCellEventsHandler(); + const focusService = useFocusService(); + const selectActions = useSelectActions(); -export function RowsRenderer(props: RowsRendererProps): ReactElement { return ( - - {props.rows.map((item, rowIndex) => { + + {rows.map((item, rowIndex) => { return ( ); })} ); -} +}); diff --git a/packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx b/packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx index 81a13fb18c..ee29bcfa20 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/Widget.tsx @@ -1,27 +1,12 @@ -import { RefreshIndicator } from "@mendix/widget-plugin-component-kit/RefreshIndicator"; -import { Pagination } from "@mendix/widget-plugin-grid/components/Pagination"; -import { FocusTargetController } from "@mendix/widget-plugin-grid/keyboard-navigation/FocusTargetController"; -import classNames from "classnames"; -import { ListActionValue, ObjectItem } from "mendix"; -import { observer } from "mobx-react-lite"; -import { CSSProperties, Fragment, ReactElement, ReactNode } from "react"; -import { - LoadingTypeEnum, - PaginationEnum, - PagingPositionEnum, - ShowPagingButtonsEnum -} from "../../typings/DatagridProps"; - +import { ReactNode } from "react"; +import { ExportProgressDialog } from "../features/data-export/ExportProgressDialog"; +import { EmptyPlaceholder } from "../features/empty-message/EmptyPlaceholder"; import { SelectAllBar } from "../features/select-all/SelectAllBar"; import { SelectionProgressDialog } from "../features/select-all/SelectionProgressDialog"; -import { SelectActionHelper } from "../helpers/SelectActionHelper"; -import { useBasicData } from "../model/hooks/injection-hooks"; -import { CellComponent, EventsController } from "../typings/CellComponent"; -import { ColumnId, GridColumn } from "../typings/GridColumn"; -import { ExportWidget } from "./ExportWidget"; import { Grid } from "./Grid"; import { GridBody } from "./GridBody"; import { GridHeader } from "./GridHeader"; +import { RefreshStatus } from "./RefreshStatus"; import { RowsRenderer } from "./RowsRenderer"; import { WidgetContent } from "./WidgetContent"; import { WidgetFooter } from "./WidgetFooter"; @@ -29,245 +14,25 @@ import { WidgetHeader } from "./WidgetHeader"; import { WidgetRoot } from "./WidgetRoot"; import { WidgetTopBar } from "./WidgetTopBar"; -export interface WidgetProps { - CellComponent: CellComponent; - className: string; - columnsDraggable: boolean; - columnsFilterable: boolean; - columnsHidable: boolean; - columnsResizable: boolean; - columnsSortable: boolean; - data: T[]; - emptyPlaceholderRenderer?: (renderWrapper: (children: ReactNode) => ReactElement) => ReactElement; - exporting: boolean; - filterRenderer: (renderWrapper: (children: ReactNode) => ReactElement, columnIndex: number) => ReactElement; - hasMoreItems: boolean; - headerContent?: ReactNode; - headerTitle?: string; - headerWrapperRenderer: (columnIndex: number, header: ReactElement) => ReactElement; - id: string; - numberOfItems?: number; - onExportCancel?: () => void; - page: number; - paginationType: PaginationEnum; - loadMoreButtonCaption?: string; - - pageSize: number; - paging: boolean; - pagingPosition: PagingPositionEnum; - showPagingButtons: ShowPagingButtonsEnum; - preview?: boolean; - processedRows: number; - rowClass?: (item: T) => string; - setPage?: (computePage: (prevPage: number) => number) => void; - styles?: CSSProperties; - rowAction?: ListActionValue; - showSelectAllToggle?: boolean; - isFirstLoad: boolean; - isFetchingNextBatch: boolean; - loadingType: LoadingTypeEnum; - columnsLoading: boolean; - showRefreshIndicator: boolean; - - // Helpers - cellEventsController: EventsController; - checkboxEventsController: EventsController; - selectActionHelper: SelectActionHelper; - focusController: FocusTargetController; - - visibleColumns: GridColumn[]; - availableColumns: GridColumn[]; - - columnsSwap: (source: ColumnId, target: [ColumnId, "after" | "before"]) => void; - setIsResizing: (status: boolean) => void; -} - -export const Widget = observer((props: WidgetProps): ReactElement => { - const { className, exporting, numberOfItems, onExportCancel, selectActionHelper } = props; - const basicData = useBasicData(); - const selectionEnabled = selectActionHelper.selectionType !== "None"; - - return ( - -
- - {exporting && ( - - )} - - ); -}); - -const Main = observer((props: WidgetProps): ReactElement => { - const { - CellComponent, - columnsHidable, - data: rows, - emptyPlaceholderRenderer, - hasMoreItems, - headerContent, - headerTitle, - loadMoreButtonCaption, - numberOfItems, - page, - pageSize, - paginationType, - paging, - pagingPosition, - preview, - showRefreshIndicator, - selectActionHelper, - setPage, - visibleColumns - } = props; - - const basicData = useBasicData(); - - const showHeader = !!headerContent; - const showTopBarPagination = paging && (pagingPosition === "top" || pagingPosition === "both"); - const showFooterPagination = paging && (pagingPosition === "bottom" || pagingPosition === "both"); - - const pagination = paging ? ( - setPage && setPage(() => page)} - nextPage={() => setPage && setPage(prev => prev + 1)} - numberOfItems={numberOfItems} - page={page} - pageSize={pageSize} - showPagingButtons={props.showPagingButtons} - previousPage={() => setPage && setPage(prev => prev - 1)} - pagination={paginationType} - /> - ) : null; - - const cssGridStyles = gridStyle(visibleColumns, { - selectItemColumn: selectActionHelper.showCheckboxColumn, - visibilitySelectorColumn: columnsHidable - }); - - const selectionEnabled = selectActionHelper.selectionType !== "None"; - +export function Widget(props: { onExportCancel?: () => void }): ReactNode { return ( - - - {showHeader && {headerContent}} + + + - - + + - {showRefreshIndicator ? : null} - - - {(rows.length === 0 || preview) && - emptyPlaceholderRenderer && - emptyPlaceholderRenderer(children => ( -
-
{children}
-
- ))} + + + +
- -
+ + + + ); -}); - -function gridStyle(columns: GridColumn[], optional: OptionalColumns): CSSProperties { - const columnSizes = columns.map(c => c.getCssWidth()); - - const sizes: string[] = []; - - if (optional.selectItemColumn) { - sizes.push("48px"); - } - - sizes.push(...columnSizes); - - if (optional.visibilitySelectorColumn) { - sizes.push("54px"); - } - - return { - gridTemplateColumns: sizes.join(" ") - }; } - -type OptionalColumns = { - selectItemColumn?: boolean; - visibilitySelectorColumn?: boolean; -}; diff --git a/packages/pluggableWidgets/datagrid-web/src/components/WidgetFooter.tsx b/packages/pluggableWidgets/datagrid-web/src/components/WidgetFooter.tsx index 161cde501f..703af7cf16 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/WidgetFooter.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/WidgetFooter.tsx @@ -1,42 +1,41 @@ import { If } from "@mendix/widget-plugin-component-kit/If"; import { observer } from "mobx-react-lite"; -import { ComponentPropsWithoutRef, ReactElement, ReactNode } from "react"; -import { PaginationEnum } from "../../typings/DatagridProps"; +import { ReactElement } from "react"; import { SelectionCounter } from "../features/selection-counter/SelectionCounter"; import { useSelectionCounterViewModel } from "../features/selection-counter/injection-hooks"; +import { useDatagridConfig, usePaginationService, useTexts } from "../model/hooks/injection-hooks"; +import { Pagination } from "./Pagination"; -type WidgetFooterProps = { - pagination: ReactNode; - paginationType: PaginationEnum; - loadMoreButtonCaption?: string; - hasMoreItems: boolean; - setPage?: (computePage: (prevPage: number) => number) => void; -} & ComponentPropsWithoutRef<"div">; - -export const WidgetFooter = observer(function WidgetFooter(props: WidgetFooterProps): ReactElement | null { - const { pagination, paginationType, loadMoreButtonCaption, hasMoreItems, setPage, ...rest } = props; +export const WidgetFooter = observer(function WidgetFooter(): ReactElement | null { + const config = useDatagridConfig(); + const paging = usePaginationService(); + const { loadMoreButtonCaption } = useTexts(); const selectionCounterVM = useSelectionCounterViewModel(); return ( -
+
- {hasMoreItems && paginationType === "loadMore" && ( +
- )} -
{pagination}
+
+
+ + + +
); diff --git a/packages/pluggableWidgets/datagrid-web/src/components/WidgetHeader.tsx b/packages/pluggableWidgets/datagrid-web/src/components/WidgetHeader.tsx index f55c0db551..aaa2fe35af 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/WidgetHeader.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/WidgetHeader.tsx @@ -1,19 +1,30 @@ -import { ComponentPropsWithoutRef, ReactElement } from "react"; +import { getGlobalFilterContextObject } from "@mendix/widget-plugin-filtering/context"; +import { getGlobalSelectionContext, useCreateSelectionContextValue } from "@mendix/widget-plugin-grid/selection"; +import { PropsWithChildren, ReactElement, ReactNode } from "react"; +import { useDatagridFilterAPI, useMainGate, useSelectionHelper, useTexts } from "../model/hooks/injection-hooks"; -type WidgetHeaderProps = { - headerTitle?: string; -} & ComponentPropsWithoutRef<"div">; +const Selection = getGlobalSelectionContext(); +const FilterAPI = getGlobalFilterContextObject(); -export function WidgetHeader(props: WidgetHeaderProps): ReactElement | null { - const { children, headerTitle, ...rest } = props; +function HeaderContainer(props: PropsWithChildren): ReactElement { + const filterAPI = useDatagridFilterAPI(); + const selectionContext = useCreateSelectionContextValue(useSelectionHelper()); + return ( + + {props.children} + + ); +} - if (!children) { - return null; - } +export const WidgetHeader = function WidgetHeader(): ReactNode { + const { headerAriaLabel } = useTexts(); + const { filtersPlaceholder } = useMainGate().props; + + if (!filtersPlaceholder) return null; return ( -
- {children} +
+ {filtersPlaceholder}
); -} +}; diff --git a/packages/pluggableWidgets/datagrid-web/src/components/WidgetHeaderContext.tsx b/packages/pluggableWidgets/datagrid-web/src/components/WidgetHeaderContext.tsx deleted file mode 100644 index 794f86029f..0000000000 --- a/packages/pluggableWidgets/datagrid-web/src/components/WidgetHeaderContext.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { getGlobalFilterContextObject } from "@mendix/widget-plugin-filtering/context"; -import { - getGlobalSelectionContext, - SelectionHelper, - useCreateSelectionContextValue -} from "@mendix/widget-plugin-grid/selection"; -import { memo, ReactElement, ReactNode } from "react"; -import { useDatagridFilterAPI } from "../model/hooks/injection-hooks"; - -interface WidgetHeaderContextProps { - children?: ReactNode; - selectionHelper?: SelectionHelper; -} - -const SelectionContext = getGlobalSelectionContext(); -const FilterContext = getGlobalFilterContextObject(); - -function HeaderContainer(props: WidgetHeaderContextProps): ReactElement { - const filterAPI = useDatagridFilterAPI(); - const selectionContext = useCreateSelectionContextValue(props.selectionHelper); - return ( - - {props.children} - - ); -} - -const component = memo(HeaderContainer); - -component.displayName = "WidgetHeaderContext"; - -// Override NamedExoticComponent type -export const WidgetHeaderContext = component as (props: WidgetHeaderContextProps) => ReactElement; diff --git a/packages/pluggableWidgets/datagrid-web/src/components/WidgetRoot.tsx b/packages/pluggableWidgets/datagrid-web/src/components/WidgetRoot.tsx index bba4ea22fa..d005ac1201 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/WidgetRoot.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/WidgetRoot.tsx @@ -1,41 +1,21 @@ import classNames from "classnames"; import { observer } from "mobx-react-lite"; -import { ComponentPropsWithoutRef, ReactElement, useMemo, useRef } from "react"; -import { useSelectionDialogViewModel } from "../features/select-all/injection-hooks"; -import { SelectionMethod } from "../helpers/SelectActionHelper"; +import { PropsWithChildren, ReactElement } from "react"; +import { useDatagridRootVM } from "../model/hooks/injection-hooks"; -type P = ComponentPropsWithoutRef<"div">; - -export interface WidgetRootProps extends P { - className?: string; - selection?: boolean; - selectionMethod: SelectionMethod; - exporting?: boolean; -} - -export const WidgetRoot = observer(function WidgetRoot(props: WidgetRootProps): ReactElement { - const ref = useRef(null); - const { className, selectionMethod, selection, exporting, children, ...rest } = props; - const { isOpen: selectingAllPages } = useSelectionDialogViewModel(); - const style = useMemo(() => { - const s = { ...props.style }; - if ((exporting || selectingAllPages) && ref.current) { - s.height = ref.current.offsetHeight; - } - return s; - }, [props.style, exporting, selectingAllPages]); +export const WidgetRoot = observer(function WidgetRoot({ children }: PropsWithChildren): ReactElement { + const vm = useDatagridRootVM(); return (
{children} diff --git a/packages/pluggableWidgets/datagrid-web/src/components/WidgetTopBar.tsx b/packages/pluggableWidgets/datagrid-web/src/components/WidgetTopBar.tsx index cdf48e1cd3..390194bc0c 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/WidgetTopBar.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/WidgetTopBar.tsx @@ -1,27 +1,27 @@ import { If } from "@mendix/widget-plugin-component-kit/If"; import { observer } from "mobx-react-lite"; -import { ComponentPropsWithoutRef, ReactElement, ReactNode } from "react"; +import { ReactElement } from "react"; import { SelectionCounter } from "../features/selection-counter/SelectionCounter"; import { useSelectionCounterViewModel } from "../features/selection-counter/injection-hooks"; +import { useDatagridConfig } from "../model/hooks/injection-hooks"; +import { Pagination } from "./Pagination"; -type WidgetTopBarProps = { - pagination: ReactNode; -} & ComponentPropsWithoutRef<"div">; - -export const WidgetTopBar = observer(function WidgetTopBar(props: WidgetTopBarProps): ReactElement { - const { pagination, ...rest } = props; - const selectionCounterVM = useSelectionCounterViewModel(); +export const WidgetTopBar = observer(function WidgetTopBar(): ReactElement { + const config = useDatagridConfig(); + const selectionCounter = useSelectionCounterViewModel(); return ( -
+
- +
- {pagination} + + +
diff --git a/packages/pluggableWidgets/datagrid-web/src/components/__tests__/Grid.spec.tsx b/packages/pluggableWidgets/datagrid-web/src/components/__tests__/Grid.spec.tsx new file mode 100644 index 0000000000..fd42081e81 --- /dev/null +++ b/packages/pluggableWidgets/datagrid-web/src/components/__tests__/Grid.spec.tsx @@ -0,0 +1,33 @@ +import { render } from "@testing-library/react"; +import { ContainerProvider } from "brandi-react"; +import { createDatagridContainer } from "../../model/containers/createDatagridContainer"; +import { mockContainerProps } from "../../utils/test-utils"; +import { Grid } from "../Grid"; + +describe("Grid", () => { + it("renders without crashing", () => { + const props = mockContainerProps(); + const [container] = createDatagridContainer(props); + const { asFragment } = render( + + Test + + ); + + expect(asFragment()).toMatchSnapshot(); + }); + + it("renders without selector column", () => { + const [container] = createDatagridContainer({ + ...mockContainerProps(), + columnsHidable: false + }); + const { asFragment } = render( + + Test + + ); + + expect(asFragment()).toMatchSnapshot(); + }); +}); diff --git a/packages/pluggableWidgets/datagrid-web/src/components/__tests__/Table.spec.tsx b/packages/pluggableWidgets/datagrid-web/src/components/__tests__/Table.spec.tsx deleted file mode 100644 index 8f9f717f6f..0000000000 --- a/packages/pluggableWidgets/datagrid-web/src/components/__tests__/Table.spec.tsx +++ /dev/null @@ -1,691 +0,0 @@ -import { ClickActionHelper } from "@mendix/widget-plugin-grid/helpers/ClickActionHelper"; -import { SelectionCounterViewModel } from "@mendix/widget-plugin-grid/main"; -import { MultiSelectionStatus, useSelectionHelper } from "@mendix/widget-plugin-grid/selection"; -import { list, listWidget, objectItems, SelectionMultiValueBuilder } from "@mendix/widget-plugin-test-utils"; -import "@testing-library/jest-dom"; -import { cleanup, getAllByRole, getByRole, queryByRole, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { ListValue, ObjectItem, SelectionMultiValue } from "mendix"; -import { ReactElement } from "react"; -import { ItemSelectionMethodEnum } from "typings/DatagridProps"; -import { CellEventsController, useCellEventsController } from "../../features/row-interaction/CellEventsController"; -import { - CheckboxEventsController, - useCheckboxEventsController -} from "../../features/row-interaction/CheckboxEventsController"; -import { SelectActionHelper, useSelectActionHelper } from "../../helpers/SelectActionHelper"; -import { LegacyContext, LegacyRootScope } from "../../helpers/root-context"; -import { GridBasicData } from "../../helpers/state/GridBasicData"; -import { GridColumn } from "../../typings/GridColumn"; -import { column, mockGridColumn, mockWidgetProps } from "../../utils/test-utils"; -import { Widget, WidgetProps } from "../Widget"; - -// you can also pass the mock implementation -// to jest.fn as an argument -window.IntersectionObserver = jest.fn(() => ({ - root: null, - rootMargin: "", - thresholds: [0, 1], - disconnect: jest.fn(), - observe: jest.fn(), - unobserve: jest.fn(), - takeRecords: jest.fn() -})); - -function withCtx( - widgetProps: WidgetProps, - contextOverrides: Partial = {} -): ReactElement { - const defaultBasicData = { - gridInteractive: false, - selectionStatus: "none" as const, - setSelectionHelper: jest.fn(), - exportDialogLabel: undefined, - cancelExportLabel: undefined, - selectRowLabel: undefined, - selectAllRowsLabel: undefined - }; - - const defaultSelectionCountStore = { - selectedCount: 0, - displayCount: "", - fmtSingular: "%d row selected", - fmtPlural: "%d rows selected" - }; - - const mockContext = { - basicData: defaultBasicData as unknown as GridBasicData, - selectionHelper: undefined, - selectActionHelper: widgetProps.selectActionHelper, - cellEventsController: widgetProps.cellEventsController, - checkboxEventsController: widgetProps.checkboxEventsController, - focusController: widgetProps.focusController, - selectionCountStore: defaultSelectionCountStore as unknown as SelectionCounterViewModel, - ...contextOverrides - }; - - return ( - - - - ); -} - -// Helper function to render Widget with root context -function renderWithRootContext( - widgetProps: WidgetProps, - contextOverrides: Partial = {} -): ReturnType { - return render(withCtx(widgetProps, contextOverrides)); -} - -// TODO: Rewrite or delete these tests -// eslint-disable-next-line jest/no-disabled-tests -describe.skip("Table", () => { - it("renders the structure correctly", () => { - const component = renderWithRootContext(mockWidgetProps()); - - expect(component.asFragment()).toMatchSnapshot(); - }); - - it("renders the structure correctly with sorting", () => { - const component = renderWithRootContext({ ...mockWidgetProps(), columnsSortable: true }); - - expect(component.asFragment()).toMatchSnapshot(); - }); - - it("renders the structure correctly with resizing", () => { - const component = renderWithRootContext({ ...mockWidgetProps(), columnsResizable: true }); - - expect(component.asFragment()).toMatchSnapshot(); - }); - - it("renders the structure correctly with dragging", () => { - const component = renderWithRootContext({ ...mockWidgetProps(), columnsDraggable: true }); - - expect(component.asFragment()).toMatchSnapshot(); - }); - - it("renders the structure correctly with filtering", () => { - const component = renderWithRootContext({ ...mockWidgetProps(), columnsFilterable: true }); - - expect(component.asFragment()).toMatchSnapshot(); - }); - - it("renders the structure correctly with hiding", () => { - const component = renderWithRootContext({ ...mockWidgetProps(), columnsHidable: true }); - - expect(component.asFragment()).toMatchSnapshot(); - }); - - it("renders the structure correctly with paging", () => { - const component = renderWithRootContext({ ...mockWidgetProps(), paging: true }); - - expect(component.asFragment()).toMatchSnapshot(); - }); - - it("renders the structure correctly with custom filtering", () => { - const props = mockWidgetProps(); - const columns = [column("Test")].map((col, index) => mockGridColumn(col, index)); - props.columnsFilterable = true; - props.visibleColumns = columns; - props.availableColumns = columns; - const component = renderWithRootContext(props); - - expect(component.asFragment()).toMatchSnapshot(); - }); - - it("renders the structure correctly with empty placeholder", () => { - const component = renderWithRootContext({ - ...mockWidgetProps(), - emptyPlaceholderRenderer: renderWrapper => renderWrapper(
) - }); - - expect(component.asFragment()).toMatchSnapshot(); - }); - - it("renders the structure correctly with column alignments", () => { - const props = mockWidgetProps(); - const columns = [ - column("Test", col => { - col.alignment = "center"; - }), - column("Test 2", col => (col.alignment = "right")) - ].map((col, index) => mockGridColumn(col, index)); - - props.visibleColumns = columns; - props.availableColumns = columns; - - const component = renderWithRootContext(props); - - expect(component.asFragment()).toMatchSnapshot(); - }); - - it("renders the structure correctly with dynamic row class", () => { - const component = renderWithRootContext({ ...mockWidgetProps(), rowClass: () => "myclass" }); - - expect(component.asFragment()).toMatchSnapshot(); - }); - - it("renders the structure correctly for preview when no header is provided", () => { - const props = mockWidgetProps(); - const columns = [column("", col => (col.alignment = "center"))].map((col, index) => mockGridColumn(col, index)); - props.preview = true; - props.visibleColumns = columns; - props.availableColumns = columns; - - const component = renderWithRootContext(props); - - expect(component.asFragment()).toMatchSnapshot(); - }); - - it("renders the structure correctly with header wrapper", () => { - const component = renderWithRootContext({ - ...mockWidgetProps(), - headerWrapperRenderer: (index, header) => ( -
- {header} -
- ) - }); - - expect(component.asFragment()).toMatchSnapshot(); - }); - - it("renders the structure correctly with header filters and a11y", () => { - const component = renderWithRootContext({ - ...mockWidgetProps(), - headerContent: ( -
- -
- ), - headerTitle: "filter title" - }); - - expect(component.asFragment()).toMatchSnapshot(); - }); - - describe("with selection method checkbox", () => { - let props: ReturnType; - - beforeEach(() => { - props = mockWidgetProps(); - props.selectActionHelper = new SelectActionHelper("Single", undefined, "checkbox", false, 5, "clear"); - props.paging = true; - props.data = objectItems(3); - }); - - it("render method class", () => { - const { container } = renderWithRootContext(props, {}); - - expect(container.firstChild).toHaveClass("widget-datagrid-selection-method-checkbox"); - }); - - it("render an extra column and add class to each selected row", () => { - props.selectActionHelper.isSelected = () => true; - - const { asFragment } = renderWithRootContext(props, {}); - - expect(asFragment()).toMatchSnapshot(); - }); - - it("render correct number of checked checkboxes", () => { - const [a, b, c, d, e, f] = (props.data = objectItems(6)); - let selection: ObjectItem[] = []; - props.selectActionHelper.isSelected = item => selection.includes(item); - - // eslint-disable-next-line @typescript-eslint/explicit-function-return-type - const getChecked = () => screen.getAllByRole("checkbox").filter(elt => elt.checked); - - const { rerender } = render(withCtx(props)); - - expect(getChecked()).toHaveLength(0); - - selection = [a, b, c]; - rerender(withCtx({ ...props, data: [a, b, c, d, e, f] })); - expect(getChecked()).toHaveLength(3); - - selection = [c]; - rerender(withCtx({ ...props, data: [a, b, c, d, e, f] })); - expect(getChecked()).toHaveLength(1); - - selection = [d, e]; - rerender(withCtx({ ...props, data: [a, b, c, d, e, f] })); - expect(getChecked()).toHaveLength(2); - - selection = [f, e, d, a]; - rerender(withCtx({ ...props, data: [a, b, c, d, e, f] })); - expect(getChecked()).toHaveLength(4); - }); - - it("call onSelect when checkbox is clicked", async () => { - const items = props.data; - const onSelect = jest.fn(); - props.selectActionHelper.onSelect = onSelect; - props.checkboxEventsController = new CheckboxEventsController( - item => ({ - item, - selectionMethod: props.selectActionHelper.selectionMethod, - selectionType: "Single", - selectionMode: "clear", - pageSize: props.pageSize - }), - onSelect, - jest.fn(), - jest.fn(), - jest.fn() - ); - - // renderWithRootContext(props, { - // basicData: { gridInteractive: true } as unknown as GridBasicData - // }); - - const checkbox1 = screen.getAllByRole("checkbox")[0]; - const checkbox3 = screen.getAllByRole("checkbox")[2]; - - await userEvent.click(checkbox1); - expect(onSelect).toHaveBeenCalledTimes(1); - expect(onSelect).toHaveBeenLastCalledWith(items[0], false, true); - - await userEvent.click(checkbox1); - expect(onSelect).toHaveBeenCalledTimes(2); - expect(onSelect).toHaveBeenLastCalledWith(items[0], false, true); - - await userEvent.click(checkbox3); - expect(onSelect).toHaveBeenCalledTimes(3); - expect(onSelect).toHaveBeenLastCalledWith(items[2], false, true); - - await userEvent.click(checkbox3); - expect(onSelect).toHaveBeenCalledTimes(4); - expect(onSelect).toHaveBeenLastCalledWith(items[2], false, true); - }); - }); - - it("not render header checkbox when showCheckboxColumn is false", () => { - const props = mockWidgetProps(); - props.data = objectItems(5); - props.paging = true; - props.selectActionHelper = new SelectActionHelper("Multi", undefined, "checkbox", false, 5, "clear"); - renderWithRootContext(props); - - const colheader = screen.getAllByRole("columnheader")[0]; - expect(queryByRole(colheader, "checkbox")).toBeNull(); - }); - - describe("with multi selection helper", () => { - it("render header checkbox if helper is given and checkbox state depends on the helper status", () => { - const props = mockWidgetProps(); - props.data = objectItems(5); - props.paging = true; - props.selectActionHelper = new SelectActionHelper("Multi", undefined, "checkbox", true, 5, "clear"); - - const renderWithStatus = (_status: MultiSelectionStatus): ReturnType => { - return renderWithRootContext(props); - }; - - renderWithStatus("none"); - expect(queryByRole(screen.getAllByRole("columnheader")[0], "checkbox")).not.toBeChecked(); - - cleanup(); - renderWithStatus("some"); - expect(queryByRole(screen.getAllByRole("columnheader")[0], "checkbox")).toBeChecked(); - - cleanup(); - renderWithStatus("all"); - expect(queryByRole(screen.getAllByRole("columnheader")[0], "checkbox")).toBeChecked(); - }); - - it("not render header checkbox if method is rowClick", () => { - const props = mockWidgetProps(); - props.selectActionHelper = new SelectActionHelper("Multi", undefined, "rowClick", false, 5, "clear"); - - renderWithRootContext(props); - - const colheader = screen.getAllByRole("columnheader")[0]; - expect(queryByRole(colheader, "checkbox")).toBeNull(); - }); - - it("call onSelectAll when header checkbox is clicked", async () => { - const props = mockWidgetProps(); - props.selectActionHelper = new SelectActionHelper("Multi", undefined, "checkbox", true, 5, "clear"); - props.selectActionHelper.onSelectAll = jest.fn(); - - renderWithRootContext(props, {}); - - const checkbox = screen.getAllByRole("checkbox")[0]; - - await userEvent.click(checkbox); - expect(props.selectActionHelper.onSelectAll).toHaveBeenCalledTimes(1); - - await userEvent.click(checkbox); - expect(props.selectActionHelper.onSelectAll).toHaveBeenCalledTimes(2); - }); - }); - - describe("with selection method rowClick", () => { - let props: ReturnType; - - beforeEach(() => { - props = mockWidgetProps(); - props.selectActionHelper = new SelectActionHelper("Single", undefined, "rowClick", true, 5, "clear"); - props.paging = true; - props.data = objectItems(3); - }); - - it("render method class", () => { - const { container } = renderWithRootContext(props, {}); - - expect(container.firstChild).toHaveClass("widget-datagrid-selection-method-click"); - }); - - it("add class to each selected cell", () => { - props.selectActionHelper.isSelected = () => true; - - const { asFragment } = renderWithRootContext(props, {}); - - expect(asFragment()).toMatchSnapshot(); - }); - - it("call onSelect when cell is clicked", async () => { - const items = props.data; - const onSelect = jest.fn(); - const columns = [column("Column A"), column("Column B")].map((col, index) => mockGridColumn(col, index)); - props.visibleColumns = columns; - props.availableColumns = columns; - props.cellEventsController = new CellEventsController( - item => ({ - item, - selectionType: props.selectActionHelper.selectionType, - selectionMethod: props.selectActionHelper.selectionMethod, - selectionMode: "clear", - clickTrigger: "none", - pageSize: props.pageSize - }), - onSelect, - jest.fn(), - jest.fn(), - jest.fn(), - jest.fn() - ); - - renderWithRootContext(props, {}); - - const rows = screen.getAllByRole("row").slice(1); - expect(rows).toHaveLength(3); - - const [row1, row2] = rows; - const [cell1, cell2] = getAllByRole(row1, "gridcell"); - const [cell3, cell4] = getAllByRole(row2, "gridcell"); - - const sleep = (t: number): Promise => new Promise(res => setTimeout(res, t)); - - // Click cell1 two times - await userEvent.click(cell1); - expect(onSelect).toHaveBeenCalledTimes(1); - expect(onSelect).toHaveBeenLastCalledWith(items[0], false, false); - await sleep(320); - - await userEvent.click(cell1); - expect(onSelect).toHaveBeenCalledTimes(2); - expect(onSelect).toHaveBeenLastCalledWith(items[0], false, false); - await sleep(320); - - // Click cell2 - await userEvent.click(cell2); - expect(onSelect).toHaveBeenCalledTimes(3); - expect(onSelect).toHaveBeenLastCalledWith(items[0], false, false); - await sleep(320); - - // Click cell3 and cell4 - await userEvent.click(cell4); - expect(onSelect).toHaveBeenCalledTimes(4); - expect(onSelect).toHaveBeenLastCalledWith(items[1], false, false); - await sleep(320); - - await userEvent.click(cell3); - expect(onSelect).toHaveBeenCalledTimes(5); - expect(onSelect).toHaveBeenLastCalledWith(items[1], false, false); - }); - }); - - describe("when selecting is enabled, allow the user to select multiple rows", () => { - let items: ReturnType; - let props: ReturnType; - let selection: SelectionMultiValue; - let ds: ListValue; - - function WidgetWithSelectionHelper({ - selectionMethod, - ...props - }: WidgetProps & { - selectionMethod: ItemSelectionMethodEnum; - }): ReactElement { - const helper = useSelectionHelper(selection, ds, undefined, "always clear"); - const selectHelper = useSelectActionHelper( - { - itemSelection: selection, - itemSelectionMethod: selectionMethod, - itemSelectionMode: "clear", - showSelectAllToggle: false, - pageSize: 5 - }, - helper - ); - const cellEventsController = useCellEventsController( - selectHelper, - new ClickActionHelper("single", null), - props.focusController - ); - - const checkboxEventsController = useCheckboxEventsController(selectHelper, props.focusController); - - const contextValue = { - basicData: { - gridInteractive: true, - selectionStatus: helper?.type === "Multi" ? helper.selectionStatus : "unknown" - } as unknown as GridBasicData, - selectionHelper: helper, - selectActionHelper: selectHelper, - cellEventsController, - checkboxEventsController, - focusController: props.focusController, - selectionCountStore: {} as unknown as SelectionCounterViewModel - }; - - return ( - - - - ); - } - - function setup( - jsx: ReactElement - ): ReturnType & { rows: HTMLElement[]; user: ReturnType } { - const result = render(jsx); - const user = userEvent.setup(); - const rows = screen.getAllByRole("row").slice(1); - - return { - user, - rows, - ...result - }; - } - - beforeEach(() => { - ds = list(20); - items = ds.items!; - props = mockWidgetProps(); - selection = new SelectionMultiValueBuilder().build(); - props.data = items; - const columns = [ - column("Name"), - column("Description"), - column("Amount", col => { - col.showContentAs = "customContent"; - col.content = listWidget(() => ); - }) - ].map((col, index) => mockGridColumn(col, index)); - - props.visibleColumns = columns; - props.availableColumns = columns; - }); - - it("selects multiple rows with shift+click on a row", async () => { - const { rows, user } = setup(); - - expect(rows).toHaveLength(20); - - await user.click(rows[10].children[2]); - expect(selection.selection).toEqual([items[10]]); - - await user.keyboard("[ShiftLeft>]"); - - await user.click(rows[14].children[2]); - expect(selection.selection).toHaveLength(5); - expect(selection.selection).toEqual(items.slice(10, 15)); - - await user.click(rows[4].children[2]); - expect(selection.selection).toHaveLength(7); - expect(selection.selection).toEqual(items.slice(4, 11)); - - await user.click(rows[8].children[2]); - expect(selection.selection).toHaveLength(3); - expect(selection.selection).toEqual(items.slice(8, 11)); - }); - - it("selects multiple rows with shift+click on a checkbox", async () => { - const { rows, user } = setup(); - - expect(rows).toHaveLength(20); - - await user.click(getByRole(rows[10], "checkbox")); - expect(selection.selection).toEqual([items[10]]); - - await user.keyboard("[ShiftLeft>]"); - - await user.click(getByRole(rows[14], "checkbox")); - expect(selection.selection).toHaveLength(5); - expect(selection.selection).toEqual(items.slice(10, 15)); - - await user.click(getByRole(rows[4], "checkbox")); - expect(selection.selection).toHaveLength(7); - expect(selection.selection).toEqual(items.slice(4, 11)); - - await user.click(getByRole(rows[8], "checkbox")); - expect(selection.selection).toHaveLength(3); - expect(selection.selection).toEqual(items.slice(8, 11)); - }); - - it("selects all available rows with metaKey+a and method checkbox", async () => { - const { rows, user } = setup(); - - expect(rows).toHaveLength(20); - - const [row] = rows; - const [checkbox] = getAllByRole(row, "checkbox"); - await user.click(checkbox); - expect(checkbox).toHaveFocus(); - expect(selection.selection).toHaveLength(1); - - await user.keyboard("{Meta>}a{/Meta}"); - expect(selection.selection).toHaveLength(20); - }); - - it("selects all available rows with metaKey+a and method rowClick", async () => { - const { rows, user } = setup(); - - expect(rows).toHaveLength(20); - - const [row] = rows; - const [cell] = getAllByRole(row, "gridcell"); - await user.click(cell); - expect(cell).toHaveFocus(); - expect(selection.selection).toHaveLength(1); - - await user.keyboard("{Meta>}a{/Meta}"); - expect(selection.selection).toHaveLength(20); - }); - - it("selects all available rows with ctrlKey+a and method checkbox", async () => { - const { rows, user } = setup(); - - expect(rows).toHaveLength(20); - - const [row] = rows; - const [checkbox] = getAllByRole(row, "checkbox"); - await user.click(checkbox); - expect(checkbox).toHaveFocus(); - expect(selection.selection).toHaveLength(1); - - await user.keyboard("{Control>}a{/Control}"); - expect(selection.selection).toHaveLength(20); - }); - - it("selects all available rows with ctrlKey+a and method rowClick", async () => { - const { rows, user } = setup(); - - expect(rows).toHaveLength(20); - - const [row] = rows; - const [cell] = getAllByRole(row, "gridcell"); - await user.click(cell); - expect(cell).toHaveFocus(); - expect(selection.selection).toHaveLength(1); - - await user.keyboard("{Control>}a{/Control}"); - expect(selection.selection).toHaveLength(20); - }); - - it("must not select rows, when metaKey+a or ctrlKey+a pressed in custom widget", async () => { - const { rows, user } = setup(); - - expect(rows).toHaveLength(20); - - const [input] = screen.getAllByRole("textbox"); - await user.click(input); - await user.keyboard("Hello, world!"); - expect(selection.selection).toHaveLength(0); - - await user.keyboard("{Control>}a{/Control}"); - expect(selection.selection).toHaveLength(0); - - await user.keyboard("{Meta>}a{/Meta}"); - expect(selection.selection).toHaveLength(0); - }); - }); - - describe("when has interactive element", () => { - it("should not prevent default on keyboard input (space and Enter)", async () => { - const items = objectItems(3); - - const props = mockWidgetProps(); - const content = listWidget(() =>