From a8e28a4f8ca90d7095f1f0ccd309eea7011be08e Mon Sep 17 00:00:00 2001 From: Amirhossein Mahmoodi Date: Tue, 16 Apr 2024 11:45:14 +0330 Subject: [PATCH] implementation of datatable --- package.json | 2 +- .../components/DataTable/body/TableBody.js | 207 ++++++++++++ .../DataTable/body/TableBodyCell.js | 307 ++++++++++++++++++ .../DataTable/body/TableBodyCellValue.js | 112 +++++++ .../components/DataTable/body/TableBodyRow.js | 254 +++++++++++++++ .../DataTable/body/TableDetailPanel.js | 93 ++++++ .../DataTable/buttons/CopyButton.js | 74 +++++ .../DataTable/buttons/GrabHandleButton.js | 54 +++ .../buttons/ShowHideColumnsButton.js | 41 +++ .../DataTable/buttons/ToggleFiltersButton.js | 4 + .../components/DataTable/head/TableHead.js | 63 ++++ .../DataTable/head/TableHeadCell.js | 284 ++++++++++++++++ .../components/DataTable/head/TableHeadRow.js | 76 +++++ src/core/components/DataTable/index.js | 19 ++ .../DataTable/menus/ActionMenuItem.js | 50 +++ .../DataTable/menus/CellActionMenu.js | 100 ++++++ .../DataTable/menus/ShowHideColumnsMenu.js | 140 ++++++++ .../menus/ShowHideColumnsMenuItems.js | 160 +++++++++ src/core/components/DataTable/table/Paper.js | 78 +++++ src/core/components/DataTable/table/Table.js | 78 +++++ .../DataTable/table/TableContainer.js | 94 ++++++ .../DataTable/table/TableLoadingOverlay.js | 48 +++ .../DataTable/toolbar/BottomToolbar.js | 88 +++++ .../DataTable/toolbar/LinearProgressBar.js | 44 +++ .../DataTable/toolbar/TablePagination.js | 217 +++++++++++++ .../toolbar/ToolbarInternalButtons.js | 47 +++ .../DataTable/toolbar/TopToolbar.js | 86 +++++ src/core/utils/theme.js | 3 +- src/core/utils/utils.js | 162 +++++++++ 29 files changed, 2983 insertions(+), 2 deletions(-) create mode 100644 src/core/components/DataTable/body/TableBody.js create mode 100644 src/core/components/DataTable/body/TableBodyCell.js create mode 100644 src/core/components/DataTable/body/TableBodyCellValue.js create mode 100644 src/core/components/DataTable/body/TableBodyRow.js create mode 100644 src/core/components/DataTable/body/TableDetailPanel.js create mode 100644 src/core/components/DataTable/buttons/CopyButton.js create mode 100644 src/core/components/DataTable/buttons/GrabHandleButton.js create mode 100644 src/core/components/DataTable/buttons/ShowHideColumnsButton.js create mode 100644 src/core/components/DataTable/buttons/ToggleFiltersButton.js create mode 100644 src/core/components/DataTable/head/TableHead.js create mode 100644 src/core/components/DataTable/head/TableHeadCell.js create mode 100644 src/core/components/DataTable/head/TableHeadRow.js create mode 100644 src/core/components/DataTable/index.js create mode 100644 src/core/components/DataTable/menus/ActionMenuItem.js create mode 100644 src/core/components/DataTable/menus/CellActionMenu.js create mode 100644 src/core/components/DataTable/menus/ShowHideColumnsMenu.js create mode 100644 src/core/components/DataTable/menus/ShowHideColumnsMenuItems.js create mode 100644 src/core/components/DataTable/table/Paper.js create mode 100644 src/core/components/DataTable/table/Table.js create mode 100644 src/core/components/DataTable/table/TableContainer.js create mode 100644 src/core/components/DataTable/table/TableLoadingOverlay.js create mode 100644 src/core/components/DataTable/toolbar/BottomToolbar.js create mode 100644 src/core/components/DataTable/toolbar/LinearProgressBar.js create mode 100644 src/core/components/DataTable/toolbar/TablePagination.js create mode 100644 src/core/components/DataTable/toolbar/ToolbarInternalButtons.js create mode 100644 src/core/components/DataTable/toolbar/TopToolbar.js create mode 100644 src/core/utils/utils.js diff --git a/package.json b/package.json index 3855313..3e7a01c 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "next": "14.1.0", "react": "^18", "react-dom": "^18", - "sass": "^1.70.0", + "sass": "^1.75.0", "stylis": "^4.3.1", "stylis-plugin-rtl": "^2.1.1" }, diff --git a/src/core/components/DataTable/body/TableBody.js b/src/core/components/DataTable/body/TableBody.js new file mode 100644 index 0000000..e1620a3 --- /dev/null +++ b/src/core/components/DataTable/body/TableBody.js @@ -0,0 +1,207 @@ +import { parseFromValuesOrFunc } from "@/core/utils/utils"; +import { TableBody, Typography } from "@mui/material"; +import { useMRT_RowVirtualizer, useMRT_Rows } from "material-react-table"; +import { memo, useMemo } from "react"; +import DataTable_TableBodyRow, { Memo_DataTable_TableBodyRow } from "./TableBodyRow"; + +const DataTable_TableBody = ({ + columnVirtualizer, + table, + ...rest +}) => { + const { + getBottomRows, + getIsSomeRowsPinned, + getRowModel, + getState, + getTopRows, + options: { + enableStickyFooter, + enableStickyHeader, + layoutMode, + localization, + memoMode, + muiTableBodyProps, + renderDetailPanel, + renderEmptyRowsFallback, + rowPinningDisplayMode, + }, + refs: { tableFooterRef, tableHeadRef, tablePaperRef }, + } = table; + const { columnFilters, globalFilter, isFullScreen, rowPinning } = getState(); + + const tableBodyProps = { + ...parseFromValuesOrFunc(muiTableBodyProps, { table }), + ...rest, + }; + + const tableHeadHeight = + ((enableStickyHeader || isFullScreen) && + tableHeadRef.current?.clientHeight) || + 0; + const tableFooterHeight = + (enableStickyFooter && tableFooterRef.current?.clientHeight) || 0; + + const pinnedRowIds = useMemo(() => { + if (!rowPinning.bottom?.length && !rowPinning.top?.length) return []; + return getRowModel() + .rows.filter((row) => row.getIsPinned()) + .map((r) => r.id); + }, [rowPinning, getRowModel().rows]); + + const rows = useMRT_Rows(table); + + const rowVirtualizer = useMRT_RowVirtualizer(table, rows); + + const { virtualRows } = rowVirtualizer ?? {}; + + const commonRowProps = { + columnVirtualizer, + numRows: rows.length, + table, + }; + + return ( + <> + {!rowPinningDisplayMode?.includes('sticky') && + getIsSomeRowsPinned('top') && ( + ({ + display: layoutMode?.startsWith('grid') ? 'grid' : undefined, + position: 'sticky', + top: tableHeadHeight - 1, + zIndex: 1, + ...(parseFromValuesOrFunc(tableBodyProps?.sx, theme)), + })} + > + {getTopRows().map((row, staticRowIndex) => { + const props = { + ...commonRowProps, + row, + staticRowIndex, + }; + return memoMode === 'rows' ? ( + + ) : ( + + ); + })} + + )} + ({ + display: layoutMode?.startsWith('grid') ? 'grid' : undefined, + height: rowVirtualizer + ? `${rowVirtualizer.getTotalSize()}px` + : undefined, + minHeight: !rows.length ? '100px' : undefined, + position: 'relative', + ...(parseFromValuesOrFunc(tableBodyProps?.sx, theme)), + })} + > + {tableBodyProps?.children ?? + (!rows.length ? ( + + + {renderEmptyRowsFallback?.({ table }) ?? ( + + {globalFilter || columnFilters.length + ? localization.noResultsFound + : localization.noRecordsToDisplay} + + )} + + + ) : ( + <> + {(virtualRows ?? rows).map((rowOrVirtualRow, staticRowIndex) => { + let row = rowOrVirtualRow; + if (rowVirtualizer) { + if (renderDetailPanel) { + if (rowOrVirtualRow.index % 2 === 1) { + return null; + } else { + staticRowIndex = rowOrVirtualRow.index / 2; + } + } else { + staticRowIndex = rowOrVirtualRow.index; + } + row = rows[staticRowIndex]; + } + const props = { + ...commonRowProps, + pinnedRowIds, + row, + rowVirtualizer, + staticRowIndex, + virtualRow: rowVirtualizer + ? (rowOrVirtualRow) + : undefined, + }; + const key = `${row.id}-${row.index}`; + return memoMode === 'rows' ? ( + + ) : ( + + ); + })} + + ))} + + {!rowPinningDisplayMode?.includes('sticky') && + getIsSomeRowsPinned('bottom') && ( + ({ + bottom: tableFooterHeight - 1, + display: layoutMode?.startsWith('grid') ? 'grid' : undefined, + position: 'sticky', + zIndex: 1, + ...(parseFromValuesOrFunc(tableBodyProps?.sx, theme)), + })} + > + {getBottomRows().map((row, staticRowIndex) => { + const props = { + ...commonRowProps, + row, + staticRowIndex, + }; + return memoMode === 'rows' ? ( + + ) : ( + + ); + })} + + )} + + ); + +} +export default DataTable_TableBody + +export const Memo_DataTable_TableBody = memo( + DataTable_TableBody, + (prev, next) => prev.table.options.data === next.table.options.data, +); \ No newline at end of file diff --git a/src/core/components/DataTable/body/TableBodyCell.js b/src/core/components/DataTable/body/TableBodyCell.js new file mode 100644 index 0000000..93c9631 --- /dev/null +++ b/src/core/components/DataTable/body/TableBodyCell.js @@ -0,0 +1,307 @@ +import { isCellEditable } from "material-react-table"; + +const { parseFromValuesOrFunc, getCommonMRTCellStyles } = require("@/core/utils/utils"); +const { useTheme } = require("@emotion/react"); +const { TableCell, Skeleton } = require("@mui/material"); +const { useState, useEffect, useMemo, memo } = require("react"); +const { default: DataTable_CopyButton } = require("../buttons/CopyButton"); +const { default: DataTable_TableBodyCellValue } = require("./TableBodyCellValue"); + +const DataTable_TableBodyCell = ({ + cell, + numRows, + rowRef, + staticColumnIndex, + staticRowIndex, + table, + ...rest +}) => { + const theme = useTheme(); + const { + getState, + options: { + columnResizeDirection, + columnResizeMode, + createDisplayMode, + editDisplayMode, + enableCellActions, + enableClickToCopy, + enableColumnOrdering, + enableColumnPinning, + enableGrouping, + layoutMode, + mrtTheme: { draggingBorderColor }, + muiSkeletonProps, + muiTableBodyCellProps, + }, + setHoveredColumn, + } = table; + const { + actionCell, + columnSizingInfo, + creatingRow, + density, + draggingColumn, + draggingRow, + editingCell, + editingRow, + hoveredColumn, + hoveredRow, + isLoading, + showSkeletons, + } = getState(); + const { column, row } = cell; + const { columnDef } = column; + const { columnDefType } = columnDef; + + const args = { cell, column, row, table }; + const tableCellProps = { + ...parseFromValuesOrFunc(muiTableBodyCellProps, args), + ...parseFromValuesOrFunc(columnDef.muiTableBodyCellProps, args), + ...rest, + }; + + const skeletonProps = parseFromValuesOrFunc(muiSkeletonProps, { + cell, + column, + row, + table, + }); + + const [skeletonWidth, setSkeletonWidth] = useState(100); + useEffect(() => { + if ((!isLoading && !showSkeletons) || skeletonWidth !== 100) return; + const size = column.getSize(); + setSkeletonWidth( + columnDefType === 'display' + ? size / 2 + : Math.round(Math.random() * (size - size / 3) + size / 3), + ); + }, [isLoading, showSkeletons]); + + const draggingBorders = useMemo(() => { + const isDraggingColumn = draggingColumn?.id === column.id; + const isHoveredColumn = hoveredColumn?.id === column.id; + const isDraggingRow = draggingRow?.id === row.id; + const isHoveredRow = hoveredRow?.id === row.id; + const isFirstColumn = column.getIsFirstColumn(); + const isLastColumn = column.getIsLastColumn(); + const isLastRow = numRows && staticRowIndex === numRows - 1; + const isResizingColumn = columnSizingInfo.isResizingColumn === column.id; + const showResizeBorder = + isResizingColumn && columnResizeMode === 'onChange'; + + const borderStyle = showResizeBorder + ? `2px solid ${draggingBorderColor} !important` + : isDraggingColumn || isDraggingRow + ? `1px dashed ${theme.palette.grey[500]} !important` + : isHoveredColumn || isHoveredRow || isResizingColumn + ? `2px dashed ${draggingBorderColor} !important` + : undefined; + + if (showResizeBorder) { + return columnResizeDirection === 'ltr' + ? { borderRight: borderStyle } + : { borderLeft: borderStyle }; + } + + return borderStyle + ? { + borderBottom: + isDraggingRow || isHoveredRow || (isLastRow && !isResizingColumn) + ? borderStyle + : undefined, + borderLeft: + isDraggingColumn || + isHoveredColumn || + ((isDraggingRow || isHoveredRow) && isFirstColumn) + ? borderStyle + : undefined, + borderRight: + isDraggingColumn || + isHoveredColumn || + ((isDraggingRow || isHoveredRow) && isLastColumn) + ? borderStyle + : undefined, + borderTop: isDraggingRow || isHoveredRow ? borderStyle : undefined, + } + : undefined; + }, [ + columnSizingInfo.isResizingColumn, + draggingColumn, + draggingRow, + hoveredColumn, + hoveredRow, + staticRowIndex, + ]); + + const isColumnPinned = + enableColumnPinning && + columnDef.columnDefType !== 'group' && + column.getIsPinned(); + + const isEditable = isCellEditable({ cell, table }); + + const isEditing = + isEditable && + !['custom', 'modal'].includes(editDisplayMode) && + (editDisplayMode === 'table' || + editingRow?.id === row.id || + editingCell?.id === cell.id) && + !row.getIsGrouped(); + + const isCreating = + isEditable && createDisplayMode === 'row' && creatingRow?.id === row.id; + + const showClickToCopyButton = + (parseFromValuesOrFunc(enableClickToCopy, cell) === true || + parseFromValuesOrFunc(columnDef.enableClickToCopy, cell) === true) && + !['context-menu', false].includes( + // @ts-ignore + parseFromValuesOrFunc(columnDef.enableClickToCopy, cell), + ); + + const isRightClickable = parseFromValuesOrFunc(enableCellActions, cell); + + const cellValueProps = { + cell, + table, + }; + + const handleDoubleClick = (event) => { + tableCellProps?.onDoubleClick?.(event); + openEditingCell({ cell, table }); + }; + + const handleDragEnter = (e) => { + tableCellProps?.onDragEnter?.(e); + if (enableGrouping && hoveredColumn?.id === 'drop-zone') { + setHoveredColumn(null); + } + if (enableColumnOrdering && draggingColumn) { + setHoveredColumn( + columnDef.enableColumnOrdering !== false ? column : null, + ); + } + }; + + const handleDragOver = (e) => { + if (columnDef.enableColumnOrdering !== false) { + e.preventDefault(); + } + }; + + const handleContextMenu = (e) => { + tableCellProps?.onContextMenu?.(e); + if (isRightClickable) { + e.preventDefault(); + table.setActionCell(cell); + table.refs.actionCellRef.current = e.currentTarget; + } + }; + + return ( + ({ + '&:hover': { + outline: + actionCell?.id === cell.id || + (editDisplayMode === 'cell' && isEditable) || + (editDisplayMode === 'table' && (isCreating || isEditing)) + ? `1px solid ${theme.palette.grey[500]}` + : undefined, + textOverflow: 'clip', + }, + alignItems: layoutMode?.startsWith('grid') ? 'center' : undefined, + cursor: isRightClickable + ? 'context-menu' + : isEditable && editDisplayMode === 'cell' + ? 'pointer' + : 'inherit', + outline: + actionCell?.id === cell.id + ? `1px solid ${theme.palette.grey[500]}` + : undefined, + outlineOffset: '-1px', + overflow: 'hidden', + p: + density === 'compact' + ? columnDefType === 'display' + ? '0 0.5rem' + : '0.5rem' + : density === 'comfortable' + ? columnDefType === 'display' + ? '0.5rem 0.75rem' + : '1rem' + : columnDefType === 'display' + ? '1rem 1.25rem' + : '1.5rem', + + textOverflow: columnDefType !== 'display' ? 'ellipsis' : undefined, + whiteSpace: + row.getIsPinned() || density === 'compact' ? 'nowrap' : 'normal', + ...getCommonMRTCellStyles({ + column, + table, + tableCellProps, + theme, + }), + ...draggingBorders, + })} + > + {tableCellProps.children ?? ( + <> + {cell.getIsPlaceholder() ? ( + columnDef.PlaceholderCell?.({ cell, column, row, table }) ?? null + ) : showSkeletons !== false && (isLoading || showSkeletons) ? ( + + ) : columnDefType === 'display' && + (['mrt-row-expand', 'mrt-row-numbers', 'mrt-row-select'].includes( + column.id, + ) || + !row.getIsGrouped()) ? ( + columnDef.Cell?.({ + cell, + column, + renderedCellValue: cell.renderValue(), + row, + rowRef, + staticColumnIndex, + staticRowIndex, + table, + }) + ) : showClickToCopyButton && columnDef.enableClickToCopy !== false ? ( + + + + ) : ( + + )} + {cell.getIsGrouped() && !columnDef.GroupedCell && ( + <> ({row.subRows?.length}) + )} + + )} + + ); + +} +export default DataTable_TableBodyCell; + +export const Memo_DataTable_TableBodyCell = memo( + DataTable_TableBodyCell, + (prev, next) => next.cell === prev.cell, +); \ No newline at end of file diff --git a/src/core/components/DataTable/body/TableBodyCellValue.js b/src/core/components/DataTable/body/TableBodyCellValue.js new file mode 100644 index 0000000..6d4f05f --- /dev/null +++ b/src/core/components/DataTable/body/TableBodyCellValue.js @@ -0,0 +1,112 @@ +import { Box } from "@mui/material"; + +const allowedTypes = ["string", "number"]; + +const DataTable_TableBodyCellValue = ({ + cell, + rowRef, + staticColumnIndex, + staticRowIndex, + table, +}) => { + const { + getState, + options: { + enableFilterMatchHighlighting, + mrtTheme: { matchHighlightColor }, + }, + } = table; + const { column, row } = cell; + const { columnDef } = column; + const { globalFilter, globalFilterFn } = getState(); + const filterValue = column.getFilterValue(); + + let renderedCellValue = + cell.getIsAggregated() && columnDef.AggregatedCell + ? columnDef.AggregatedCell({ + cell, + column, + row, + table, + }) + : row.getIsGrouped() && !cell.getIsGrouped() + ? null + : cell.getIsGrouped() && columnDef.GroupedCell + ? columnDef.GroupedCell({ + cell, + column, + row, + table, + }) + : undefined; + + const isGroupedValue = renderedCellValue !== undefined; + + if (!isGroupedValue) { + renderedCellValue = cell.renderValue(); + } + + if ( + enableFilterMatchHighlighting && + columnDef.enableFilterMatchHighlighting !== false && + String(renderedCellValue) && + allowedTypes.includes(typeof renderedCellValue) && + ((filterValue && + allowedTypes.includes(typeof filterValue) && + ["autocomplete", "text"].includes(columnDef.filterVariant)) || + (globalFilter && + allowedTypes.includes(typeof globalFilter) && + column.getCanGlobalFilter())) + ) { + const chunks = highlightWords?.({ + matchExactly: + (filterValue ? columnDef._filterFn : globalFilterFn) !== "fuzzy", + query: (filterValue ?? globalFilter ?? "").toString(), + text: renderedCellValue?.toString(), + }); + if (chunks?.length > 1 || chunks?.[0]?.match) { + renderedCellValue = ( + + {chunks?.map(({ key, match, text }) => ( + + )) ?? renderedCellValue} + + ); + } + } + + if (columnDef.Cell && !isGroupedValue) { + renderedCellValue = columnDef.Cell({ + cell, + column, + renderedCellValue, + row, + rowRef, + staticColumnIndex, + staticRowIndex, + table, + }); + } + + return renderedCellValue; +}; +export default DataTable_TableBodyCellValue; diff --git a/src/core/components/DataTable/body/TableBodyRow.js b/src/core/components/DataTable/body/TableBodyRow.js new file mode 100644 index 0000000..e500eb0 --- /dev/null +++ b/src/core/components/DataTable/body/TableBodyRow.js @@ -0,0 +1,254 @@ +import { commonCellBeforeAfterStyles, getCommonPinnedCellStyles, parseFromValuesOrFunc } from "@/core/utils/utils"; +import { useTheme } from "@emotion/react"; +import { TableRow, alpha, darken, lighten } from "@mui/material"; +import { getIsRowSelected } from "material-react-table"; +import { memo, useMemo, useRef } from "react"; +import DataTable_TableBodyCell, { Memo_DataTable_TableBodyCell } from "./TableBodyCell"; +import DataTable_TableDetailPanel from "./TableDetailPanel"; + +const DataTable_TableBodyRow = ({ + columnVirtualizer, + numRows, + pinnedRowIds, + row, + rowVirtualizer, + staticRowIndex, + table, + virtualRow, + ...rest +}) => { + const theme = useTheme(); + + const { + getState, + options: { + enableRowOrdering, + enableRowPinning, + enableStickyFooter, + enableStickyHeader, + layoutMode, + memoMode, + mrtTheme: { + baseBackgroundColor, + pinnedRowBackgroundColor, + selectedRowBackgroundColor, + }, + muiTableBodyRowProps, + renderDetailPanel, + rowPinningDisplayMode, + }, + refs: { tableFooterRef, tableHeadRef }, + setHoveredRow, + } = table; + const { + density, + draggingColumn, + draggingRow, + editingCell, + editingRow, + hoveredRow, + isFullScreen, + rowPinning, + } = getState(); + + const visibleCells = row.getVisibleCells(); + + const { virtualColumns, virtualPaddingLeft, virtualPaddingRight } = + columnVirtualizer ?? {}; + + const isRowSelected = getIsRowSelected({ row, table }); + const isRowPinned = enableRowPinning && row.getIsPinned(); + const isDraggingRow = draggingRow?.id === row.id; + const isHoveredRow = hoveredRow?.id === row.id; + + const tableRowProps = { + ...parseFromValuesOrFunc(muiTableBodyRowProps, { + row, + staticRowIndex, + table, + }), + ...rest, + }; + + const [bottomPinnedIndex, topPinnedIndex] = useMemo(() => { + if ( + !enableRowPinning || + !rowPinningDisplayMode?.includes('sticky') || + !pinnedRowIds || + !row.getIsPinned() + ) + return []; + return [ + [...pinnedRowIds].reverse().indexOf(row.id), + pinnedRowIds.indexOf(row.id), + ]; + }, [pinnedRowIds, rowPinning]); + + const tableHeadHeight = + ((enableStickyHeader || isFullScreen) && + tableHeadRef.current?.clientHeight) || + 0; + const tableFooterHeight = + (enableStickyFooter && tableFooterRef.current?.clientHeight) || 0; + + const sx = parseFromValuesOrFunc(tableRowProps?.sx, theme); + + const defaultRowHeight = + density === 'compact' ? 37 : density === 'comfortable' ? 53 : 69; + + const customRowHeight = + // @ts-ignore + parseInt(tableRowProps?.style?.height ?? sx?.height, 10) || undefined; + + const rowHeight = customRowHeight || defaultRowHeight; + + const handleDragEnter = (_e) => { + if (enableRowOrdering && draggingRow) { + setHoveredRow(row); + } + }; + + const handleDragOver = (e) => { + e.preventDefault(); + }; + + const rowRef = useRef(null); + + const cellHighlightColor = isRowSelected + ? selectedRowBackgroundColor + : isRowPinned + ? pinnedRowBackgroundColor + : undefined; + + const cellHighlightColorHover = + tableRowProps?.hover !== false + ? isRowSelected + ? cellHighlightColor + : theme.palette.mode === 'dark' + ? `${lighten(baseBackgroundColor, 0.3)}` + : `${darken(baseBackgroundColor, 0.3)}` + : undefined; + + return ( + <> + { + if (node) { + rowRef.current = node; + rowVirtualizer?.measureElement(node); + } + }} + selected={isRowSelected} + {...tableRowProps} + style={{ + transform: virtualRow + ? `translateY(${virtualRow.start}px)` + : undefined, + ...tableRowProps?.style, + }} + sx={(theme) => ({ + '&:hover td:after': cellHighlightColorHover + ? { + backgroundColor: alpha(cellHighlightColorHover, 0.3), + ...commonCellBeforeAfterStyles, + } + : undefined, + backgroundColor: `${baseBackgroundColor} !important`, + bottom: + !virtualRow && bottomPinnedIndex !== undefined && isRowPinned + ? `${bottomPinnedIndex * rowHeight + + (enableStickyFooter ? tableFooterHeight - 1 : 0) + }px` + : undefined, + boxSizing: 'border-box', + display: layoutMode?.startsWith('grid') ? 'flex' : undefined, + opacity: isRowPinned ? 0.97 : isDraggingRow || isHoveredRow ? 0.5 : 1, + position: virtualRow + ? 'absolute' + : rowPinningDisplayMode?.includes('sticky') && isRowPinned + ? 'sticky' + : 'relative', + td: { + ...getCommonPinnedCellStyles({ table, theme }), + }, + 'td:after': cellHighlightColor + ? { + backgroundColor: cellHighlightColor, + ...commonCellBeforeAfterStyles, + } + : undefined, + top: virtualRow + ? 0 + : topPinnedIndex !== undefined && isRowPinned + ? `${topPinnedIndex * rowHeight + + (enableStickyHeader || isFullScreen ? tableHeadHeight - 1 : 0) + }px` + : undefined, + transition: virtualRow ? 'none' : 'all 150ms ease-in-out', + width: '100%', + zIndex: + rowPinningDisplayMode?.includes('sticky') && isRowPinned ? 2 : 0, + ...(sx), + })} + > + {virtualPaddingLeft ? ( + + ) : null} + {(virtualColumns ?? visibleCells).map( + (cellOrVirtualCell, staticColumnIndex) => { + let cell = cellOrVirtualCell; + if (columnVirtualizer) { + staticColumnIndex = (cellOrVirtualCell).index; + cell = visibleCells[staticColumnIndex]; + } + const props = { + cell, + numRows, + rowRef, + staticColumnIndex, + staticRowIndex, + table, + }; + return cell ? ( + memoMode === 'cells' && + cell.column.columnDef.columnDefType === 'data' && + !draggingColumn && + !draggingRow && + editingCell?.id !== cell.id && + editingRow?.id !== row.id ? ( + + ) : ( + + ) + ) : null; + }, + )} + {virtualPaddingRight ? ( + + ) : null} + + {renderDetailPanel && !row.getIsGrouped() && ( + + )} + + ); +} +export default DataTable_TableBodyRow + +export const Memo_DataTable_TableBodyRow = memo( + DataTable_TableBodyRow, + (prev, next) => + prev.row === next.row && prev.staticRowIndex === next.staticRowIndex, +); \ No newline at end of file diff --git a/src/core/components/DataTable/body/TableDetailPanel.js b/src/core/components/DataTable/body/TableDetailPanel.js new file mode 100644 index 0000000..0c9f8a9 --- /dev/null +++ b/src/core/components/DataTable/body/TableDetailPanel.js @@ -0,0 +1,93 @@ +import { parseFromValuesOrFunc } from "@/core/utils/utils"; +import { Collapse, TableCell, TableRow } from "@mui/material"; + +const DataTable_TableDetailPanel = ({ + parentRowRef, + row, + rowVirtualizer, + staticRowIndex, + table, + virtualRow, + ...rest +}) => { + const { + getState, + getVisibleLeafColumns, + options: { + layoutMode, + mrtTheme: { baseBackgroundColor }, + muiDetailPanelProps, + muiTableBodyRowProps, + renderDetailPanel, + }, + } = table; + const { isLoading } = getState(); + + + const tableRowProps = parseFromValuesOrFunc(muiTableBodyRowProps, { + isDetailPanel: true, + row, + staticRowIndex, + table, + }); + + const tableCellProps = { + ...parseFromValuesOrFunc(muiDetailPanelProps, { + row, + table, + }), + ...rest, + }; + + const DetailPanel = !isLoading && renderDetailPanel?.({ row, table }); + + return ( + { + if (node) { + rowVirtualizer?.measureElement?.(node); + } + }} + {...tableRowProps} + sx={(theme) => ({ + display: layoutMode?.startsWith('grid') ? 'flex' : undefined, + position: virtualRow ? 'absolute' : undefined, + top: virtualRow + ? `${parentRowRef.current?.getBoundingClientRect()?.height}px` + : undefined, + transform: virtualRow + ? `translateY(${virtualRow?.start}px)` + : undefined, + width: '100%', + ...(parseFromValuesOrFunc(tableRowProps?.sx, theme)), + })} + > + ({ + backgroundColor: virtualRow ? baseBackgroundColor : undefined, + borderBottom: !row.getIsExpanded() ? 'none' : undefined, + display: layoutMode?.startsWith('grid') ? 'flex' : undefined, + py: !!DetailPanel && row.getIsExpanded() ? '1rem' : 0, + transition: !virtualRow ? 'all 150ms ease-in-out' : undefined, + width: `100%`, + ...(parseFromValuesOrFunc(tableCellProps?.sx, theme)), + })} + > + {virtualRow ? ( + row.getIsExpanded() && DetailPanel + ) : ( + + {DetailPanel} + + )} + + + ); + +} +export default DataTable_TableDetailPanel \ No newline at end of file diff --git a/src/core/components/DataTable/buttons/CopyButton.js b/src/core/components/DataTable/buttons/CopyButton.js new file mode 100644 index 0000000..0ffe871 --- /dev/null +++ b/src/core/components/DataTable/buttons/CopyButton.js @@ -0,0 +1,74 @@ +import { getCommonTooltipProps, parseFromValuesOrFunc } from "@/core/utils/utils"; +import { Button, Tooltip } from "@mui/material"; + +const DataTable_CopyButton = ({ + cell, + table, + ...rest +}) => { + const { + options: { localization, muiCopyButtonProps }, + } = table; + const { column, row } = cell; + const { columnDef } = column; + + const [copied, setCopied] = useState(false); + + const handleCopy = (event, text) => { + event.stopPropagation(); + navigator.clipboard.writeText(text); + setCopied(true); + setTimeout(() => setCopied(false), 4000); + }; + + const buttonProps = { + ...parseFromValuesOrFunc(muiCopyButtonProps, { + cell, + column, + row, + table, + }), + ...parseFromValuesOrFunc(columnDef.muiCopyButtonProps, { + cell, + column, + row, + table, + }), + ...rest, + }; + + return ( + + + )} + {enableColumnOrdering && ( + + )} + {enableColumnPinning && ( + + )} + {enableHiding && ( + + )} + + + {allColumns.map((column, index) => ( + + ))} + + ); +} +export default DataTable_ShowHideColumnsMenu \ No newline at end of file diff --git a/src/core/components/DataTable/menus/ShowHideColumnsMenuItems.js b/src/core/components/DataTable/menus/ShowHideColumnsMenuItems.js new file mode 100644 index 0000000..58425d9 --- /dev/null +++ b/src/core/components/DataTable/menus/ShowHideColumnsMenuItems.js @@ -0,0 +1,160 @@ +import { getCommonTooltipProps, parseFromValuesOrFunc } from "@/core/utils/utils"; +import { Box, FormControlLabel, MenuItem, Switch, Tooltip, Typography } from "@mui/material"; +import { useRef, useState } from "react"; +import DataTable_GrabHandleButton from "../buttons/GrabHandleButton"; + +const DataTable_ShowHideColumnsMenuItems = ({ + allColumns, + column, + hoveredColumn, + isNestedColumns, + setHoveredColumn, + table, + ...rest +}) => { + const { + getState, + options: { + enableColumnOrdering, + enableHiding, + localization, + mrtTheme: { draggingBorderColor }, + }, + setColumnOrder, + } = table; + + const { columnOrder } = getState(); + const { columnDef } = column; + const { columnDefType } = columnDef; + + const switchChecked = column.getIsVisible(); + + const handleToggleColumnHidden = (column) => { + if (columnDefType === 'group') { + column?.columns?.forEach?.((childColumn) => { + childColumn.toggleVisibility(!switchChecked); + }); + } else { + column.toggleVisibility(); + } + }; + + const menuItemRef = useRef(null); + + const [isDragging, setIsDragging] = useState(false); + + const handleDragStart = (e) => { + setIsDragging(true); + try { + e.dataTransfer.setDragImage(menuItemRef.current, 0, 0); + } catch (e) { + console.error(e); + } + }; + + const handleDragEnd = (_e) => { + setIsDragging(false); + setHoveredColumn(null); + if (hoveredColumn) { + setColumnOrder(reorderColumn(column, hoveredColumn, columnOrder)); + } + }; + + const handleDragEnter = (_e) => { + if (!isDragging && columnDef.enableColumnOrdering !== false) { + setHoveredColumn(column); + } + }; + + if (!columnDef.header || columnDef.visibleInShowHideMenu === false) { + return null; + } + + return ( + <> + ({ + alignItems: 'center', + justifyContent: 'flex-start', + my: 0, + opacity: isDragging ? 0.5 : 1, + outline: isDragging + ? `2px dashed ${theme.palette.grey[500]}` + : hoveredColumn?.id === column.id + ? `2px dashed ${draggingBorderColor}` + : 'none', + outlineOffset: '-2px', + pl: `${(column.depth + 0.5) * 2}rem`, + py: '6px', + ...(parseFromValuesOrFunc(rest?.sx, theme)), + })} + > + + {columnDefType !== 'group' && + enableColumnOrdering && + !isNestedColumns && + (columnDef.enableColumnOrdering !== false ? ( + + ) : ( + + ))} + {enableHiding ? ( + + + + } + disabled={!column.getCanHide()} + label={columnDef.header} + onChange={() => handleToggleColumnHidden(column)} + /> + ) : ( + + {columnDef.header} + + )} + + + {column.columns?.map((c, i) => ( + + ))} + + ); + +} +export default DataTable_ShowHideColumnsMenuItems; \ No newline at end of file diff --git a/src/core/components/DataTable/table/Paper.js b/src/core/components/DataTable/table/Paper.js new file mode 100644 index 0000000..db3bbec --- /dev/null +++ b/src/core/components/DataTable/table/Paper.js @@ -0,0 +1,78 @@ +import { parseFromValuesOrFunc } from "@/core/utils/utils"; +import { Paper } from "@mui/material"; +import DataTable_BottomToolbar from "../toolbar/BottomToolbar"; +import DataTable_TopToolbar from "../toolbar/TopToolbar"; +import DataTable_TableContainer from "./TableContainer"; + +const DataTable_Paper = ({ table, ...rest }) => { + const { + getState, + options: { + enableBottomToolbar, + enableTopToolbar, + mrtTheme: { baseBackgroundColor }, + muiTablePaperProps, + renderBottomToolbar, + renderTopToolbar, + }, + refs: { tablePaperRef }, + } = table; + + const { isFullScreen } = getState(); + + const paperProps = { + ...parseFromValuesOrFunc(muiTablePaperProps, { table }), + ...rest, + }; + + return ( + { + tablePaperRef.current = ref; + if (paperProps?.ref) { + //@ts-ignore + paperProps.ref.current = ref; + } + }} + style={{ + ...(isFullScreen + ? { + bottom: 0, + height: '100dvh', + left: 0, + margin: 0, + maxHeight: '100dvh', + maxWidth: '100dvw', + padding: 0, + position: 'fixed', + right: 0, + top: 0, + width: '100dvw', + zIndex: 999, + } + : {}), + ...paperProps?.style, + }} + sx={(theme) => ({ + backgroundColor: baseBackgroundColor, + backgroundImage: 'unset', + overflow: 'hidden', + transition: 'all 100ms ease-in-out', + ...(parseFromValuesOrFunc(paperProps?.sx, theme)), + })} + > + {enableTopToolbar && + (parseFromValuesOrFunc(renderTopToolbar, { table }) ?? ( + + ))} + + {enableBottomToolbar && + (parseFromValuesOrFunc(renderBottomToolbar, { table }) ?? ( + + ))} + + ); +}; +export default DataTable_Paper; diff --git a/src/core/components/DataTable/table/Table.js b/src/core/components/DataTable/table/Table.js new file mode 100644 index 0000000..2bbe1a9 --- /dev/null +++ b/src/core/components/DataTable/table/Table.js @@ -0,0 +1,78 @@ +import { parseCSSVarId, parseFromValuesOrFunc } from "@/core/utils/utils"; +import { Table } from "@mui/material"; +import { useMRT_ColumnVirtualizer } from "material-react-table"; +import { useMemo } from "react"; +import DataTable_TableBody, { Memo_DataTable_TableBody } from "../body/TableBody"; +import DataTable_TableHead from "../head/TableHead"; + +const DataTable_Table = ({ + table, + ...rest +}) => { + const { + getFlatHeaders, + getState, + options: { + columns, + enableStickyHeader, + enableTableFooter, + enableTableHead, + layoutMode, + memoMode, + muiTableProps, + renderCaption, + }, + } = table; + const { columnSizing, columnSizingInfo, columnVisibility, isFullScreen } = + getState(); + + const tableProps = { + ...parseFromValuesOrFunc(muiTableProps, { table }), + ...rest, + }; + + const Caption = parseFromValuesOrFunc(renderCaption, { table }); + + const columnSizeVars = useMemo(() => { + const headers = getFlatHeaders(); + const colSizes = {}; + for (let i = 0; i < headers.length; i++) { + const header = headers[i]; + const colSize = header.getSize(); + colSizes[`--header-${parseCSSVarId(header.id)}-size`] = colSize; + colSizes[`--col-${parseCSSVarId(header.column.id)}-size`] = colSize; + } + return colSizes; + }, [columns, columnSizing, columnSizingInfo, columnVisibility]); + + const columnVirtualizer = useMRT_ColumnVirtualizer(table); + + const commonTableGroupProps = { + columnVirtualizer, + table, + }; + + return ( + ({ + borderCollapse: 'separate', + display: layoutMode?.startsWith('grid') ? 'grid' : undefined, + position: 'relative', + ...(parseFromValuesOrFunc(tableProps?.sx, theme)), + })} + > + {!!Caption && } + {enableTableHead && } + {memoMode === 'table-body' || columnSizingInfo.isResizingColumn ? ( + + ) : ( + + )} + {/* {enableTableFooter && } */} +
{Caption}
+ ); +} +export default DataTable_Table \ No newline at end of file diff --git a/src/core/components/DataTable/table/TableContainer.js b/src/core/components/DataTable/table/TableContainer.js new file mode 100644 index 0000000..aa4b625 --- /dev/null +++ b/src/core/components/DataTable/table/TableContainer.js @@ -0,0 +1,94 @@ +import { parseFromValuesOrFunc } from "@/core/utils/utils"; +import { TableContainer } from "@mui/material"; +import { useEffect, useLayoutEffect, useState } from "react"; +import DataTable_CellActionMenu from "../menus/CellActionMenu"; +import DataTable_Table from "./Table"; +import DataTable_TableLoadingOverlay from "./TableLoadingOverlay"; + +const useIsomorphicLayoutEffect = + typeof window !== 'undefined' ? useLayoutEffect : useEffect; + +const DataTable_TableContainer = ({ + table, + ...rest +}) => { + const { + getState, + options: { + enableCellActions, + enableStickyHeader, + muiTableContainerProps, + }, + refs: { bottomToolbarRef, tableContainerRef, topToolbarRef }, + } = table; + const { + actionCell, + isFullScreen, + isLoading, + showLoadingOverlay, + } = getState(); + + const loading = + showLoadingOverlay !== false && (isLoading || showLoadingOverlay); + + const [totalToolbarHeight, setTotalToolbarHeight] = useState(0); + + const tableContainerProps = { + ...parseFromValuesOrFunc(muiTableContainerProps, { + table, + }), + ...rest, + }; + + useIsomorphicLayoutEffect(() => { + const topToolbarHeight = + typeof document !== 'undefined' + ? topToolbarRef.current?.offsetHeight ?? 0 + : 0; + + const bottomToolbarHeight = + typeof document !== 'undefined' + ? bottomToolbarRef?.current?.offsetHeight ?? 0 + : 0; + + setTotalToolbarHeight(topToolbarHeight + bottomToolbarHeight); + }); + + return ( + { + if (node) { + tableContainerRef.current = node; + if (tableContainerProps?.ref) { + //@ts-ignore + tableContainerProps.ref.current = node; + } + } + }} + style={{ + maxHeight: isFullScreen + ? `calc(100vh - ${totalToolbarHeight}px)` + : undefined, + ...tableContainerProps?.style, + }} + sx={(theme) => ({ + maxHeight: enableStickyHeader + ? `clamp(350px, calc(100vh - ${totalToolbarHeight}px), 9999px)` + : undefined, + maxWidth: '100%', + overflow: 'auto', + position: 'relative', + ...(parseFromValuesOrFunc(tableContainerProps?.sx, theme)), + })} + > + {loading ? : null} + + {enableCellActions && actionCell && } + + ); + +} +export default DataTable_TableContainer \ No newline at end of file diff --git a/src/core/components/DataTable/table/TableLoadingOverlay.js b/src/core/components/DataTable/table/TableLoadingOverlay.js new file mode 100644 index 0000000..55e4ae5 --- /dev/null +++ b/src/core/components/DataTable/table/TableLoadingOverlay.js @@ -0,0 +1,48 @@ +import { Box, CircularProgress } from "@mui/material"; + +const DataTable_TableLoadingOverlay = ({ + table, + ...rest +}) => { + const { + options: { + localization, + mrtTheme: { baseBackgroundColor }, + muiCircularProgressProps, + }, + } = table; + + const circularProgressProps = { + ...parseFromValuesOrFunc(muiCircularProgressProps, { table }), + ...rest, + }; + + return ( + + {circularProgressProps?.Component ?? ( + + )} + + ); + +} +export default DataTable_TableLoadingOverlay \ No newline at end of file diff --git a/src/core/components/DataTable/toolbar/BottomToolbar.js b/src/core/components/DataTable/toolbar/BottomToolbar.js new file mode 100644 index 0000000..15e7f5a --- /dev/null +++ b/src/core/components/DataTable/toolbar/BottomToolbar.js @@ -0,0 +1,88 @@ +import { getCommonToolbarStyles, parseFromValuesOrFunc } from "@/core/utils/utils"; +import { Box, alpha, useMediaQuery } from "@mui/material"; +import DataTable_LinearProgressBar from "./LinearProgressBar"; +import DataTable_TablePagination from "./TablePagination"; + +const DataTable_BottomToolbar = ({ + table, + ...rest +}) => { + const { + getState, + options: { + enablePagination, + muiBottomToolbarProps, + positionPagination, + renderBottomToolbarCustomActions, + }, + refs: { bottomToolbarRef }, + } = table; + const { isFullScreen } = getState(); + + const isMobile = useMediaQuery('(max-width:720px)'); + const toolbarProps = { + ...parseFromValuesOrFunc(muiBottomToolbarProps, { table }), + ...rest, + }; + + const stackAlertBanner = isMobile || !!renderBottomToolbarCustomActions; + return ( + { + if (node) { + bottomToolbarRef.current = node; + if (toolbarProps?.ref) { + // @ts-ignore + toolbarProps.ref.current = node; + } + } + }} + sx={(theme) => ({ + ...getCommonToolbarStyles({ table, theme }), + bottom: isFullScreen ? '0' : undefined, + boxShadow: `0 1px 2px -1px ${alpha( + theme.palette.grey[700], + 0.5, + )} inset`, + left: 0, + position: isFullScreen ? 'fixed' : 'relative', + right: 0, + ...(parseFromValuesOrFunc(toolbarProps?.sx, theme)), + })} + > + + + {renderBottomToolbarCustomActions ? ( + renderBottomToolbarCustomActions({ table }) + ) : ( + + )} + + {enablePagination && + ['both', 'bottom'].includes(positionPagination ?? '') && ( + + )} + + + + ); +} +export default DataTable_BottomToolbar \ No newline at end of file diff --git a/src/core/components/DataTable/toolbar/LinearProgressBar.js b/src/core/components/DataTable/toolbar/LinearProgressBar.js new file mode 100644 index 0000000..cdcbc0a --- /dev/null +++ b/src/core/components/DataTable/toolbar/LinearProgressBar.js @@ -0,0 +1,44 @@ +import { parseFromValuesOrFunc } from "@/core/utils/utils"; +import { Collapse, LinearProgress } from "@mui/material"; + +const DataTable_LinearProgressBar = ({ + isTopToolbar, + table, + ...rest +}) => { + const { + getState, + options: { muiLinearProgressProps }, + } = table; + const { isSaving, showProgressBars } = getState(); + + const linearProgressProps = { + ...parseFromValuesOrFunc(muiLinearProgressProps, { + isTopToolbar, + table, + }), + ...rest, + }; + + return ( + + + + ); +} +export default DataTable_LinearProgressBar \ No newline at end of file diff --git a/src/core/components/DataTable/toolbar/TablePagination.js b/src/core/components/DataTable/toolbar/TablePagination.js new file mode 100644 index 0000000..cd70fda --- /dev/null +++ b/src/core/components/DataTable/toolbar/TablePagination.js @@ -0,0 +1,217 @@ +import { flipIconStyles, getCommonTooltipProps, parseFromValuesOrFunc } from "@/core/utils/utils"; +import { useTheme } from "@emotion/react"; +import { Box, IconButton, InputLabel, MenuItem, Pagination, PaginationItem, Select, Tooltip, Typography, useMediaQuery } from "@mui/material"; + +const defaultRowsPerPage = [5, 10, 15, 20, 25, 30, 50, 100]; + +const DataTable_TablePagination = ({ + position = 'bottom', + table, + ...rest +}) => { + const theme = useTheme(); + const isMobile = useMediaQuery('(max-width: 720px)'); + + const { + getState, + options: { + enableToolbarInternalActions, + icons: { ChevronLeftIcon, ChevronRightIcon, FirstPageIcon, LastPageIcon }, + localization, + muiPaginationProps, + paginationDisplayMode, + }, + } = table; + + const { + pagination: { pageIndex = 0, pageSize = 10 }, + showGlobalFilter, + } = getState(); + + const paginationProps = { + ...parseFromValuesOrFunc(muiPaginationProps, { + table, + }), + ...rest, + }; + + const totalRowCount = table.getRowCount(); + const numberOfPages = table.getPageCount(); + const showFirstLastPageButtons = numberOfPages > 2; + const firstRowIndex = pageIndex * pageSize; + const lastRowIndex = Math.min(pageIndex * pageSize + pageSize, totalRowCount); + + const { + SelectProps = {}, + disabled = false, + rowsPerPageOptions = defaultRowsPerPage, + showFirstButton = showFirstLastPageButtons, + showLastButton = showFirstLastPageButtons, + showRowsPerPage = true, + ...restPaginationProps + } = paginationProps ?? {}; + + const disableBack = pageIndex <= 0 || disabled; + const disableNext = lastRowIndex >= totalRowCount || disabled; + + if (isMobile && SelectProps?.native !== false) { + SelectProps.native = true; + } + + const tooltipProps = getCommonTooltipProps(); + + return ( + + {showRowsPerPage && ( + + + {localization.rowsPerPage} + + + + )} + {paginationDisplayMode === 'pages' ? ( + table.setPageIndex(newPageIndex - 1)} + page={pageIndex + 1} + renderItem={(item) => ( + + )} + showFirstButton={showFirstButton} + showLastButton={showLastButton} + {...restPaginationProps} + /> + ) : paginationDisplayMode === 'default' ? ( + <> + {`${lastRowIndex === 0 ? 0 : (firstRowIndex + 1).toLocaleString() + }-${lastRowIndex.toLocaleString()} ${localization.of + } ${totalRowCount.toLocaleString()}`} + + {showFirstButton && ( + + + table.firstPage()} + size="small" + > + + + + + )} + + + table.previousPage()} + size="small" + > + + + + + + + table.nextPage()} + size="small" + > + + + + + {showLastButton && ( + + + table.lastPage()} + size="small" + > + + + + + )} + + + ) : null} + + ); +} +export default DataTable_TablePagination \ No newline at end of file diff --git a/src/core/components/DataTable/toolbar/ToolbarInternalButtons.js b/src/core/components/DataTable/toolbar/ToolbarInternalButtons.js new file mode 100644 index 0000000..606add0 --- /dev/null +++ b/src/core/components/DataTable/toolbar/ToolbarInternalButtons.js @@ -0,0 +1,47 @@ +import { parseFromValuesOrFunc } from "@/core/utils/utils"; +import { Box } from "@mui/material"; +import DataTable_ShowHideColumnsButton from "../buttons/ShowHideColumnsButton"; +import DataTable_ToggleFiltersButton from "../buttons/ToggleFiltersButton"; + +const DataTable_ToolbarInternalButtons = ({ table, ...rest }) => { + const { + options: { + columnFilterDisplayMode, + enableColumnFilters, + enableColumnOrdering, + enableColumnPinning, + enableFilters, + enableHiding, + renderToolbarInternalActions, + }, + } = table; + + + return ( + ({ + alignItems: 'center', + display: 'flex', + zIndex: 3, + ...(parseFromValuesOrFunc(rest?.sx, theme)), + })} + > + {renderToolbarInternalActions?.({ + table, + }) ?? ( + <> + {enableFilters && + enableColumnFilters && + columnFilterDisplayMode !== 'popover' && ( + + )} + {(enableHiding || enableColumnOrdering || enableColumnPinning) && ( + + )} + + )} + + ); +} +export default DataTable_ToolbarInternalButtons \ No newline at end of file diff --git a/src/core/components/DataTable/toolbar/TopToolbar.js b/src/core/components/DataTable/toolbar/TopToolbar.js new file mode 100644 index 0000000..4eb0aba --- /dev/null +++ b/src/core/components/DataTable/toolbar/TopToolbar.js @@ -0,0 +1,86 @@ +import { getCommonToolbarStyles, parseFromValuesOrFunc } from "@/core/utils/utils"; +import { Box, useMediaQuery } from "@mui/material"; +import DataTable_LinearProgressBar from "./LinearProgressBar"; +import DataTable_TablePagination from "./TablePagination"; +import DataTable_ToolbarInternalButtons from "./ToolbarInternalButtons"; + +const DataTable_TopToolbar = ({ table }) => { + const { + getState, + options: { + enablePagination, + enableToolbarInternalActions, + muiTopToolbarProps, + positionPagination, + renderTopToolbarCustomActions, + }, + refs: { topToolbarRef }, + } = table; + const { isFullScreen, showGlobalFilter } = getState(); + + const isMobile = useMediaQuery('(max-width:720px)'); + const isTablet = useMediaQuery('(max-width:1024px)'); + + const toolbarProps = parseFromValuesOrFunc(muiTopToolbarProps, { table }); + + const stackAlertBanner = + isMobile || + !!renderTopToolbarCustomActions || + (showGlobalFilter && isTablet); + + return ( + { + topToolbarRef.current = ref; + if (toolbarProps?.ref) { + // @ts-ignore + toolbarProps.ref.current = ref; + } + }} + sx={(theme) => ({ + ...getCommonToolbarStyles({ table, theme }), + position: isFullScreen ? 'sticky' : 'relative', + top: isFullScreen ? '0' : undefined, + ...(parseFromValuesOrFunc(toolbarProps?.sx, theme)), + })} + > + + {renderTopToolbarCustomActions?.({ table }) ?? } + {enableToolbarInternalActions && ( + + + + )} + + {enablePagination && + ['both', 'top'].includes(positionPagination ?? '') && ( + + )} + + + ); +} + +export default DataTable_TopToolbar \ No newline at end of file diff --git a/src/core/utils/theme.js b/src/core/utils/theme.js index 8f2bbec..3abbb52 100644 --- a/src/core/utils/theme.js +++ b/src/core/utils/theme.js @@ -1,10 +1,11 @@ 'use client' -import {createTheme} from "@mui/material"; +import { createTheme } from "@mui/material"; const theme = createTheme({ direction: 'rtl', typography: { fontFamily: `IRANSansFaNum, sans-serif`, + fontSize: 12 }, }); diff --git a/src/core/utils/utils.js b/src/core/utils/utils.js new file mode 100644 index 0000000..8fe3844 --- /dev/null +++ b/src/core/utils/utils.js @@ -0,0 +1,162 @@ +import { alpha, darken } from "@mui/material"; + +export const parseCSSVarId = (id) => id.replace(/[^a-zA-Z0-9]/g, '_'); + +export const parseFromValuesOrFunc = ( + fn, + arg +) => (fn instanceof Function ? fn(arg) : fn); + +export const getCommonToolbarStyles = ({ + table, +}) => ({ + alignItems: 'flex-start', + backgroundColor: table.options.mrtTheme.baseBackgroundColor, + display: 'grid', + flexWrap: 'wrap-reverse', + minHeight: '3.5rem', + overflow: 'hidden', + position: 'relative', + transition: 'all 150ms ease-in-out', + zIndex: 1, +}); + +export const getCommonTooltipProps = ( + placement, +) => ({ + disableInteractive: true, + enterDelay: 1000, + enterNextDelay: 1000, + placement, +}); + +export const flipIconStyles = (theme) => + theme.direction === 'rtl' + ? { style: { transform: 'scaleX(-1)' } } + : undefined; + +export const getCommonPinnedCellStyles = ({ + column, + table, + theme, +}) => { + const { baseBackgroundColor } = table.options.mrtTheme; + const isPinned = column?.getIsPinned(); + + return { + '&[data-pinned="true"]': { + '&:before': { + backgroundColor: alpha( + darken( + baseBackgroundColor, + theme.palette.mode === 'dark' ? 0.05 : 0.01, + ), + 0.97, + ), + boxShadow: column + ? isPinned === 'left' && column.getIsLastColumn(isPinned) + ? `-4px 0 4px -4px ${alpha(theme.palette.grey[700], 0.5)} inset` + : isPinned === 'right' && column.getIsFirstColumn(isPinned) + ? `4px 0 4px -4px ${alpha(theme.palette.grey[700], 0.5)} inset` + : undefined + : undefined, + ...commonCellBeforeAfterStyles, + }, + }, + }; +}; + +export const getCommonMRTCellStyles = ({ + column, + header, + table, + tableCellProps, + theme, +}) => { + const { + getState, + options: { enableColumnVirtualization, layoutMode }, + } = table; + const { draggingColumn } = getState(); + const { columnDef } = column; + const { columnDefType } = columnDef; + + const isColumnPinned = + columnDef.columnDefType !== 'group' && column.getIsPinned(); + + const widthStyles = { + minWidth: `max(calc(var(--${header ? 'header' : 'col'}-${parseCSSVarId( + header?.id ?? column.id, + )}-size) * 1px), ${columnDef.minSize ?? 30}px)`, + width: `calc(var(--${header ? 'header' : 'col'}-${parseCSSVarId( + header?.id ?? column.id, + )}-size) * 1px)`, + }; + + if (layoutMode === 'grid') { + widthStyles.flex = `${[0, false].includes(columnDef.grow) + ? 0 + : `var(--${header ? 'header' : 'col'}-${parseCSSVarId( + header?.id ?? column.id, + )}-size)` + } 0 auto`; + } else if (layoutMode === 'grid-no-grow') { + widthStyles.flex = `${+(columnDef.grow || 0)} 0 auto`; + } + + const pinnedStyles = isColumnPinned + ? { + ...getCommonPinnedCellStyles({ column, table, theme }), + left: + isColumnPinned === 'left' + ? `${column.getStart('left')}px` + : undefined, + opacity: 0.97, + position: 'sticky', + right: + isColumnPinned === 'right' + ? `${column.getAfter('right')}px` + : undefined, + } + : {}; + + return { + backgroundColor: 'inherit', + backgroundImage: 'inherit', + display: layoutMode?.startsWith('grid') ? 'flex' : undefined, + justifyContent: + columnDefType === 'group' + ? 'center' + : layoutMode?.startsWith('grid') + ? tableCellProps.align + : undefined, + opacity: + table.getState().draggingColumn?.id === column.id || + table.getState().hoveredColumn?.id === column.id + ? 0.5 + : 1, + position: 'relative', + transition: enableColumnVirtualization + ? 'none' + : `padding 150ms ease-in-out`, + zIndex: + column.getIsResizing() || draggingColumn?.id === column.id + ? 2 + : columnDefType !== 'group' && isColumnPinned + ? 1 + : 0, + ...pinnedStyles, + ...widthStyles, + ...(parseFromValuesOrFunc(tableCellProps?.sx, theme)), + }; +}; + +export const commonCellBeforeAfterStyles = { + content: '""', + height: '100%', + left: 0, + position: 'absolute', + top: 0, + width: '100%', + zIndex: -1, +}; \ No newline at end of file