diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/ResizeableTable.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/ResizeableTable.tsx new file mode 100644 index 000000000..d810296b2 --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/tableComp/ResizeableTable.tsx @@ -0,0 +1,222 @@ +import { default as Table, TableProps, ColumnType } from "antd/es/table"; +import React, { useCallback, useMemo, useRef, useState } from "react"; +import { Resizable } from "react-resizable"; +import styled from "styled-components"; +import _ from "lodash"; +import { useUserViewMode } from "util/hooks"; +import { ReactRef, ResizeHandleAxis } from "layout/gridLayoutPropTypes"; +import { COL_MIN_WIDTH, RecordType, CustomColumnType } from "./tableUtils"; +import { RowColorViewType, RowHeightViewType } from "./tableTypes"; +import { TableColumnStyleType, TableColumnLinkStyleType } from "comps/controls/styleControlConstants"; +import { CellColorViewType } from "./column/tableColumnComp"; +import { TableCellView } from "./TableCell"; +import { TableRowView } from "./TableRow"; + +const TitleResizeHandle = styled.span` + position: absolute; + top: 0; + right: -5px; + width: 10px; + height: 100%; + cursor: col-resize; + z-index: 1; +`; + +const TableTh = styled.th<{ width?: number }>` + overflow: hidden; + + > div { + overflow: hidden; + white-space: pre; + text-overflow: ellipsis; + } + + ${(props) => props.width && `width: ${props.width}px`}; +`; + +const ResizeableTitle = React.forwardRef((props, ref) => { + const { onResize, onResizeStop, width, viewModeResizable, ...restProps } = props; + const [childWidth, setChildWidth] = useState(0); + const resizeRef = useRef(null); + const isUserViewMode = useUserViewMode(); + + const updateChildWidth = useCallback(() => { + if (resizeRef.current) { + const width = resizeRef.current.getBoundingClientRect().width; + setChildWidth(width); + } + }, []); + + React.useEffect(() => { + updateChildWidth(); + const resizeObserver = new ResizeObserver(() => { + updateChildWidth(); + }); + + if (resizeRef.current) { + resizeObserver.observe(resizeRef.current); + } + + return () => { + resizeObserver.disconnect(); + }; + }, [updateChildWidth]); + + React.useImperativeHandle(ref, () => resizeRef.current!, []); + + const isNotDataColumn = _.isNil(restProps.title); + if ((isUserViewMode && !restProps.viewModeResizable) || isNotDataColumn) { + return ; + } + + return ( + 0 ? width : childWidth} + height={0} + onResize={(e: React.SyntheticEvent, { size }: { size: { width: number } }) => { + e.stopPropagation(); + onResize(size.width); + }} + onResizeStart={(e: React.SyntheticEvent) => { + updateChildWidth(); + e.stopPropagation(); + e.preventDefault(); + }} + onResizeStop={onResizeStop} + draggableOpts={{ enableUserSelectHack: false }} + handle={(axis: ResizeHandleAxis, ref: ReactRef) => ( + { + e.preventDefault(); + e.stopPropagation(); + }} + /> + )} + > + + + ); +}); + +type CustomTableProps = Omit, "components" | "columns"> & { + columns: CustomColumnType[]; + viewModeResizable: boolean; + visibleResizables: boolean; + rowColorFn: RowColorViewType; + rowHeightFn: RowHeightViewType; + columnsStyle: TableColumnStyleType; + size?: string; + rowAutoHeight?: boolean; + customLoading?: boolean; + onCellClick: (columnName: string, dataIndex: string) => void; + virtual?: boolean; + scroll?: { + x?: number | string; + y?: number | string; + }; +}; + +function ResizeableTableComp(props: CustomTableProps) { + const { + columns, + viewModeResizable, + visibleResizables, + rowColorFn, + rowHeightFn, + columnsStyle, + size, + rowAutoHeight, + customLoading, + onCellClick, + ...restProps + } = props; + const [resizeData, setResizeData] = useState({ index: -1, width: -1 }); + + // Memoize resize handlers + const handleResize = useCallback((width: number, index: number) => { + setResizeData({ index, width }); + }, []); + + const handleResizeStop = useCallback((width: number, index: number, onWidthResize?: (width: number) => void) => { + setResizeData({ index: -1, width: -1 }); + if (onWidthResize) { + onWidthResize(width); + } + }, []); + + // Memoize cell handlers + const createCellHandler = useCallback((col: CustomColumnType) => { + return (record: RecordType, index: number) => ({ + record, + title: String(col.dataIndex), + rowColorFn, + rowHeightFn, + cellColorFn: col.cellColorFn, + rowIndex: index, + columnsStyle, + columnStyle: col.style, + linkStyle: col.linkStyle, + tableSize: size, + autoHeight: rowAutoHeight, + onClick: () => onCellClick(col.titleText, String(col.dataIndex)), + loading: customLoading, + customAlign: col.align, + }); + }, [rowColorFn, rowHeightFn, columnsStyle, size, rowAutoHeight, onCellClick, customLoading]); + + // Memoize header cell handlers + const createHeaderCellHandler = useCallback((col: CustomColumnType, index: number, resizeWidth: number) => { + return () => ({ + width: resizeWidth, + title: col.titleText, + viewModeResizable, + onResize: (width: React.SyntheticEvent) => { + if (width) { + handleResize(Number(width), index); + } + }, + onResizeStop: (e: React.SyntheticEvent, { size }: { size: { width: number } }) => { + handleResizeStop(size.width, index, col.onWidthResize); + }, + }); + }, [viewModeResizable, handleResize, handleResizeStop]); + + // Memoize columns to prevent unnecessary re-renders + const memoizedColumns = useMemo(() => { + return columns.map((col, index) => { + const { width, style, linkStyle, cellColorFn, onWidthResize, ...restCol } = col; + const resizeWidth = (resizeData.index === index ? resizeData.width : col.width) ?? 0; + + const column: ColumnType = { + ...restCol, + width: typeof resizeWidth === "number" && resizeWidth > 0 ? resizeWidth : undefined, + minWidth: typeof resizeWidth === "number" && resizeWidth > 0 ? undefined : COL_MIN_WIDTH, + onCell: (record: RecordType, index?: number) => createCellHandler(col)(record, index ?? 0), + onHeaderCell: () => createHeaderCellHandler(col, index, Number(resizeWidth))(), + }; + return column; + }); + }, [columns, resizeData, createCellHandler, createHeaderCellHandler]); + + return ( + + components={{ + header: { + cell: ResizeableTitle, + }, + body: { + cell: TableCellView, + row: TableRowView, + }, + }} + {...restProps} + pagination={false} + columns={memoizedColumns} + /> + ); +} +ResizeableTableComp.whyDidYouRender = true; + +export const ResizeableTable = React.memo(ResizeableTableComp) as typeof ResizeableTableComp; +export type { CustomTableProps }; diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/TableCell.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/TableCell.tsx new file mode 100644 index 000000000..6e144b7ab --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/tableComp/TableCell.tsx @@ -0,0 +1,237 @@ +import React, { useContext, useMemo, useState } from "react"; +import styled, { css } from "styled-components"; +import { TableCellContext, TableRowContext } from "./tableContext"; +import { TableColumnStyleType, TableColumnLinkStyleType, ThemeDetail } from "comps/controls/styleControlConstants"; +import { RowColorViewType, RowHeightViewType } from "./tableTypes"; +import { CellColorViewType } from "./column/tableColumnComp"; +import { RecordType, OB_ROW_ORI_INDEX } from "./tableUtils"; +import { defaultTheme } from "@lowcoder-ee/constants/themeConstants"; +import Skeleton from "antd/es/skeleton"; +import { SkeletonButtonProps } from "antd/es/skeleton/Button"; + +interface TableTdProps { + $background: string; + $style: TableColumnStyleType & { rowHeight?: string }; + $defaultThemeDetail: ThemeDetail; + $linkStyle?: TableColumnLinkStyleType; + $isEditing: boolean; + $tableSize?: string; + $autoHeight?: boolean; + $customAlign?: 'left' | 'center' | 'right'; +} + +const TableTd = styled.td` + .ant-table-row-expand-icon, + .ant-table-row-indent { + display: ${(props) => (props.$isEditing ? "none" : "initial")}; + } + &.ant-table-row-expand-icon-cell { + background: ${(props) => props.$background}; + border-color: ${(props) => props.$style.border}; + } + background: ${(props) => props.$background} !important; + border-color: ${(props) => props.$style.border} !important; + border-radius: ${(props) => props.$style.radius}; + padding: 0 !important; + text-align: ${(props) => props.$customAlign || 'left'} !important; + + > div:not(.editing-border, .editing-wrapper), + .editing-wrapper .ant-input, + .editing-wrapper .ant-input-number, + .editing-wrapper .ant-picker { + margin: ${(props) => props.$isEditing ? '0px' : props.$style.margin}; + color: ${(props) => props.$style.text}; + font-weight: ${(props) => props.$style.textWeight}; + font-family: ${(props) => props.$style.fontFamily}; + overflow: hidden; + display: flex; + justify-content: ${(props) => props.$customAlign === 'center' ? 'center' : props.$customAlign === 'right' ? 'flex-end' : 'flex-start'}; + align-items: center; + text-align: ${(props) => props.$customAlign || 'left'}; + box-sizing: border-box; + ${(props) => props.$tableSize === 'small' && ` + padding: 1px 8px; + font-size: ${props.$defaultThemeDetail.textSize == props.$style.textSize ? '14px !important' : props.$style.textSize + ' !important'}; + font-style:${props.$style.fontStyle} !important; + min-height: ${props.$style.rowHeight || '14px'}; + line-height: 20px; + ${!props.$autoHeight && ` + overflow-y: auto; + max-height: ${props.$style.rowHeight || '28px'}; + `}; + `}; + ${(props) => props.$tableSize === 'middle' && ` + padding: 8px 8px; + font-size: ${props.$defaultThemeDetail.textSize == props.$style.textSize ? '16px !important' : props.$style.textSize + ' !important'}; + font-style:${props.$style.fontStyle} !important; + min-height: ${props.$style.rowHeight || '24px'}; + line-height: 24px; + ${!props.$autoHeight && ` + overflow-y: auto; + max-height: ${props.$style.rowHeight || '48px'}; + `}; + `}; + ${(props) => props.$tableSize === 'large' && ` + padding: 16px 16px; + font-size: ${props.$defaultThemeDetail.textSize == props.$style.textSize ? '18px !important' : props.$style.textSize + ' !important'}; + font-style:${props.$style.fontStyle} !important; + min-height: ${props.$style.rowHeight || '48px'}; + ${!props.$autoHeight && ` + overflow-y: auto; + max-height: ${props.$style.rowHeight || '96px'}; + `}; + `}; + + > .ant-badge > .ant-badge-status-text, + > div > .markdown-body { + color: ${(props) => props.$style.text}; + } + + > div > svg g { + stroke: ${(props) => props.$style.text}; + } + + // dark link|links color + > a, + > div a { + color: ${(props) => props.$linkStyle?.text}; + + &:hover { + color: ${(props) => props.$linkStyle?.hoverText}; + } + + &:active { + color: ${(props) => props.$linkStyle?.activeText}; + } + } + } +`; + +const TableTdLoading = styled(Skeleton.Button)` + width: 90% !important; + display: table !important; + + .ant-skeleton-button { + min-width: auto !important; + display: block !important; + ${(props) => props.$tableSize === 'small' && ` + height: 20px !important; + `} + ${(props) => props.$tableSize === 'middle' && ` + height: 24px !important; + `} + ${(props) => props.$tableSize === 'large' && ` + height: 28px !important; + `} + } +`; + +export const TableCellView = React.forwardRef((props, ref) => { + const { + record, + title, + rowIndex, + rowColorFn, + rowHeightFn, + cellColorFn, + children, + columnsStyle, + columnStyle, + linkStyle, + tableSize, + autoHeight, + loading, + customAlign, + ...restProps + } = props; + + const [editing, setEditing] = useState(false); + const rowContext = useContext(TableRowContext); + + // Memoize style calculations + const style = useMemo(() => { + if (!record) return null; + const rowColor = rowColorFn({ + currentRow: record, + currentIndex: rowIndex, + currentOriginalIndex: record[OB_ROW_ORI_INDEX], + columnTitle: title, + }); + const rowHeight = rowHeightFn({ + currentRow: record, + currentIndex: rowIndex, + currentOriginalIndex: record[OB_ROW_ORI_INDEX], + columnTitle: title, + }); + const cellColor = cellColorFn({ + currentCell: record[title], + currentRow: record, + }); + + return { + background: cellColor || rowColor || columnStyle.background || columnsStyle.background, + margin: columnStyle.margin || columnsStyle.margin, + text: columnStyle.text || columnsStyle.text, + border: columnStyle.border || columnsStyle.border, + radius: columnStyle.radius || columnsStyle.radius, + // borderWidth: columnStyle.borderWidth || columnsStyle.borderWidth, + textSize: columnStyle.textSize || columnsStyle.textSize, + textWeight: columnsStyle.textWeight || columnStyle.textWeight, + fontFamily: columnsStyle.fontFamily || columnStyle.fontFamily, + fontStyle: columnsStyle.fontStyle || columnStyle.fontStyle, + rowHeight: rowHeight, + }; + }, [record, rowIndex, title, rowColorFn, rowHeightFn, cellColorFn, columnStyle, columnsStyle]); + + if (!record) { + return ( + + {children} + + ); + } + + let { background } = style!; + if (rowContext.hover) { + background = 'transparent'; + } + + return ( + + + {loading + ? + : children + } + + + ); +}); diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/TableRow.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/TableRow.tsx new file mode 100644 index 000000000..84dbbc380 --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/tableComp/TableRow.tsx @@ -0,0 +1,27 @@ +import React, { useCallback, useState } from "react"; +import { TableRowContext } from "./tableContext"; + +export const TableRowView = React.forwardRef((props, ref) => { + const [hover, setHover] = useState(false); + const [selected, setSelected] = useState(false); + + // Memoize event handlers + const handleMouseEnter = useCallback(() => setHover(true), []); + const handleMouseLeave = useCallback(() => setHover(false), []); + const handleFocus = useCallback(() => setSelected(true), []); + const handleBlur = useCallback(() => setSelected(false), []); + + return ( + + + + ); +}); diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/hooks/useTableConfiguration.ts b/client/packages/lowcoder/src/comps/comps/tableComp/hooks/useTableConfiguration.ts new file mode 100644 index 000000000..ab1499b42 --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/tableComp/hooks/useTableConfiguration.ts @@ -0,0 +1,109 @@ +// hooks/useTableConfiguration.ts +import { useMemo, useState, useEffect, useRef } from 'react'; +import { VIRTUAL_ROW_HEIGHTS, VIRTUAL_THRESHOLD, MIN_VIRTUAL_HEIGHT, TOOLBAR_HEIGHT, HEADER_HEIGHT } from '../tableUtils'; + +// ============= HOOK 1: TABLE MODE ============= +export function useTableMode(autoHeight: boolean) { + return useMemo(() => ({ + isAutoMode: autoHeight, + isFixedMode: !autoHeight + }), [autoHeight]); +} + +// ============= HOOK 2: CONTAINER HEIGHT MEASUREMENT ============= +export function useContainerHeight(enabled: boolean) { + const [containerHeight, setContainerHeight] = useState(0); + const containerRef = useRef(null); + + useEffect(() => { + const element = containerRef.current; + if (!enabled || !element) return; + + const measureHeight = () => { + if (element) { + setContainerHeight(element.clientHeight); + } + }; + + measureHeight(); + const resizeObserver = new ResizeObserver(measureHeight); + resizeObserver.observe(element); + + return () => { + resizeObserver.disconnect(); + }; + }, [enabled]); + + return { containerHeight, containerRef }; +} + +// ============= HOOK 3: VIRTUALIZATION CALCULATION ============= +export function useVirtualization( + containerHeight: number, + dataLength: number, + tableSize: 'small' | 'middle' | 'large', + config: { + showToolbar: boolean; + showHeader: boolean; + stickyToolbar: boolean; + isFixedMode: boolean; + } +) { + return useMemo(() => { + if (!config.isFixedMode) { + return { + enabled: false, + scrollY: undefined, + itemHeight: VIRTUAL_ROW_HEIGHTS[tableSize], + reason: 'auto_mode' + }; + } + + // Calculate reserved space + const toolbarSpace = config.showToolbar && config.stickyToolbar ? TOOLBAR_HEIGHT : 0; + const headerSpace = config.showHeader ? HEADER_HEIGHT : 0; + const availableHeight = containerHeight - toolbarSpace - headerSpace; + + // Check if virtualization should be enabled + const canVirtualize = availableHeight > MIN_VIRTUAL_HEIGHT; + const hasEnoughData = dataLength >= VIRTUAL_THRESHOLD; + const enabled = canVirtualize && hasEnoughData; + + return { + enabled, + scrollY: availableHeight > 0 ? availableHeight : undefined, + itemHeight: VIRTUAL_ROW_HEIGHTS[tableSize], + reason: !canVirtualize + ? 'insufficient_height' + : !hasEnoughData + ? 'insufficient_data' + : 'enabled' + }; + }, [containerHeight, dataLength, tableSize, config]); +} + +// ============= HOOK 4: SCROLL CONFIGURATION ============= +export function useScrollConfiguration( + virtualizationEnabled: boolean, + scrollY: number | undefined, + totalColumnsWidth: number +) { + return useMemo(() => { + const baseScroll = { x: totalColumnsWidth }; + + if (!virtualizationEnabled || !scrollY) { + return { + scroll: baseScroll, + virtual: false + }; + } + + return { + scroll: { + x: totalColumnsWidth, + y: scrollY + }, + virtual: true + }; + }, [virtualizationEnabled, scrollY, totalColumnsWidth]); +} \ No newline at end of file diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/tableCompView.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/tableCompView.tsx index dc6c88b0d..dcae40952 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/tableCompView.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/tableCompView.tsx @@ -1,4 +1,3 @@ -import { default as Table, TableProps, ColumnType } from "antd/es/table"; import { TableCellContext, TableRowContext } from "comps/comps/tableComp/tableContext"; import { TableToolbar } from "comps/comps/tableComp/tableToolbarComp"; import { RowColorViewType, RowHeightViewType, TableEventOptionValues } from "comps/comps/tableComp/tableTypes"; @@ -7,7 +6,6 @@ import { COLUMN_CHILDREN_KEY, ColumnsAggrData, columnsToAntdFormat, - CustomColumnType, OB_ROW_ORI_INDEX, onTableChange, RecordType, @@ -26,787 +24,39 @@ import { } from "comps/controls/styleControlConstants"; import { CompNameContext, EditorContext } from "comps/editorState"; import { BackgroundColorContext } from "comps/utils/backgroundColorContext"; -import { PrimaryColor } from "constants/style"; import { trans } from "i18n"; import _, { isEqual } from "lodash"; -import { darkenColor, isDarkColor, isValidColor, ScrollBar } from "lowcoder-design"; -import React, { Children, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; -import { Resizable } from "react-resizable"; -import styled, { css } from "styled-components"; +import { ScrollBar } from "lowcoder-design"; +import React, { Children, useCallback, useContext, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react"; import { useMergeCompStyles, useUserViewMode } from "util/hooks"; import { TableImplComp } from "./tableComp"; import { useResizeDetector } from "react-resize-detector"; import { SlotConfigContext } from "comps/controls/slotControl"; import { EmptyContent } from "pages/common/styledComponent"; import { messageInstance } from "lowcoder-design/src/components/GlobalInstances"; -import { ReactRef, ResizeHandleAxis } from "layout/gridLayoutPropTypes"; -import { CellColorViewType } from "./column/tableColumnComp"; -import { defaultTheme } from "@lowcoder-ee/constants/themeConstants"; import { childrenToProps } from "@lowcoder-ee/comps/generators/multi"; -import { getVerticalMargin } from "@lowcoder-ee/util/cssUtil"; import { TableSummary } from "./tableSummaryComp"; -import Skeleton from "antd/es/skeleton"; -import { SkeletonButtonProps } from "antd/es/skeleton/Button"; import { ThemeContext } from "@lowcoder-ee/comps/utils/themeContext"; import { useUpdateEffect } from "react-use"; +import { ResizeableTable, CustomTableProps } from "./ResizeableTable"; +import { BackgroundWrapper, TableWrapper } from "./tableStyles"; +import { + useTableMode, + useContainerHeight, + useVirtualization, + useScrollConfiguration +} from './hooks/useTableConfiguration'; export const EMPTY_ROW_KEY = 'empty_row'; -function genLinerGradient(color: string) { - return isValidColor(color) ? `linear-gradient(${color}, ${color})` : color; -} - -const getStyle = ( - style: TableStyleType, - rowStyle: TableRowStyleType, - headerStyle: TableHeaderStyleType, - toolbarStyle: TableToolbarStyleType, -) => { - const background = genLinerGradient(style.background); - const selectedRowBackground = genLinerGradient(rowStyle.selectedRowBackground); - const hoverRowBackground = genLinerGradient(rowStyle.hoverRowBackground); - const alternateBackground = genLinerGradient(rowStyle.alternateBackground); - - return css` - .ant-table-body { - background: ${genLinerGradient(style.background)}; - } - .ant-table-tbody { - > tr:nth-of-type(2n + 1) { - background: ${genLinerGradient(rowStyle.background)}; - } - - > tr:nth-of-type(2n) { - background: ${alternateBackground}; - } - - // selected row - > tr:nth-of-type(2n + 1).ant-table-row-selected { - background: ${selectedRowBackground}, ${rowStyle.background} !important; - > td.ant-table-cell { - background: transparent !important; - } - - // > td.ant-table-cell-row-hover, - &:hover { - background: ${hoverRowBackground}, ${selectedRowBackground}, ${rowStyle.background} !important; - } - } - - > tr:nth-of-type(2n).ant-table-row-selected { - background: ${selectedRowBackground}, ${alternateBackground} !important; - > td.ant-table-cell { - background: transparent !important; - } - - // > td.ant-table-cell-row-hover, - &:hover { - background: ${hoverRowBackground}, ${selectedRowBackground}, ${alternateBackground} !important; - } - } - - // hover row - > tr:nth-of-type(2n + 1):hover { - background: ${hoverRowBackground}, ${rowStyle.background} !important; - > td.ant-table-cell-row-hover { - background: transparent; - } - } - > tr:nth-of-type(2n):hover { - background: ${hoverRowBackground}, ${alternateBackground} !important; - > td.ant-table-cell-row-hover { - background: transparent; - } - } - - > tr.ant-table-expanded-row { - background: ${background}; - } - } - `; -}; - -const TitleResizeHandle = styled.span` - position: absolute; - top: 0; - right: -5px; - width: 10px; - height: 100%; - cursor: col-resize; - z-index: 1; -`; - -const BackgroundWrapper = styled.div<{ - $style: TableStyleType; - $tableAutoHeight: boolean; - $showHorizontalScrollbar: boolean; - $showVerticalScrollbar: boolean; - $fixedToolbar: boolean; -}>` - display: flex; - flex-direction: column; - background: ${(props) => props.$style.background} !important; - border-radius: ${(props) => props.$style.radius} !important; - padding: ${(props) => props.$style.padding} !important; - margin: ${(props) => props.$style.margin} !important; - border-style: ${(props) => props.$style.borderStyle} !important; - border-width: ${(props) => `${props.$style.borderWidth} !important`}; - border-color: ${(props) => `${props.$style.border} !important`}; - height: calc(100% - ${(props) => props.$style.margin && getVerticalMargin(props.$style.margin.split(' '))}); - overflow: hidden; - - > div.table-scrollbar-wrapper { - overflow: auto; - ${(props) => props.$fixedToolbar && `height: auto`}; - - ${(props) => (props.$showHorizontalScrollbar || props.$showVerticalScrollbar) && ` - .simplebar-content-wrapper { - overflow: auto !important; - } - `} - - ${(props) => !props.$showHorizontalScrollbar && ` - div.simplebar-horizontal { - visibility: hidden !important; - } - `} - ${(props) => !props.$showVerticalScrollbar && ` - div.simplebar-vertical { - visibility: hidden !important; - } - `} - } -`; - -// TODO: find a way to limit the calc function for max-height only to first Margin value -const TableWrapper = styled.div<{ - $style: TableStyleType; - $headerStyle: TableHeaderStyleType; - $toolbarStyle: TableToolbarStyleType; - $rowStyle: TableRowStyleType; - $toolbarPosition: "above" | "below" | "close"; - $fixedHeader: boolean; - $fixedToolbar: boolean; - $visibleResizables: boolean; - $showHRowGridBorder?: boolean; -}>` - .ant-table-wrapper { - border-top: unset; - border-color: inherit; - } - - .ant-table-row-expand-icon { - color: ${PrimaryColor}; - } - - .ant-table .ant-table-cell-with-append .ant-table-row-expand-icon { - margin: 0; - top: 18px; - left: 4px; - } - - .ant-table.ant-table-small .ant-table-cell-with-append .ant-table-row-expand-icon { - top: 10px; - } - - .ant-table.ant-table-middle .ant-table-cell-with-append .ant-table-row-expand-icon { - top: 14px; - margin-right:5px; - } - - .ant-table { - background: ${(props) =>props.$style.background}; - .ant-table-container { - border-left: unset; - border-top: none !important; - border-inline-start: none !important; - - &::after { - box-shadow: none !important; - } - - .ant-table-content { - overflow: unset !important - } - - // A table expand row contains table - .ant-table-tbody .ant-table-wrapper:only-child .ant-table { - margin: 0; - } - - table { - border-top: unset; - - > .ant-table-thead { - ${(props) => - props.$fixedHeader && ` - position: sticky; - position: -webkit-sticky; - // top: ${props.$fixedToolbar ? '47px' : '0'}; - top: 0; - z-index: 2; - ` - } - > tr { - background: ${(props) => props.$headerStyle.headerBackground}; - } - > tr > th { - background: transparent; - border-color: ${(props) => props.$headerStyle.border}; - border-width: ${(props) => props.$headerStyle.borderWidth}; - color: ${(props) => props.$headerStyle.headerText}; - // border-inline-end: ${(props) => `${props.$headerStyle.borderWidth} solid ${props.$headerStyle.border}`} !important; - - /* Proper styling for fixed header cells */ - &.ant-table-cell-fix-left, &.ant-table-cell-fix-right { - z-index: 1; - background: ${(props) => props.$headerStyle.headerBackground}; - } - - - - > div { - margin: ${(props) => props.$headerStyle.margin}; - - &, .ant-table-column-title > div { - font-size: ${(props) => props.$headerStyle.textSize}; - font-weight: ${(props) => props.$headerStyle.textWeight}; - font-family: ${(props) => props.$headerStyle.fontFamily}; - font-style: ${(props) => props.$headerStyle.fontStyle}; - color:${(props) => props.$headerStyle.text} - } - } - - &:last-child { - border-inline-end: none !important; - } - &.ant-table-column-has-sorters:hover { - background-color: ${(props) => darkenColor(props.$headerStyle.headerBackground, 0.05)}; - } - - > .ant-table-column-sorters > .ant-table-column-sorter { - color: ${(props) => props.$headerStyle.headerText === defaultTheme.textDark ? "#bfbfbf" : props.$headerStyle.headerText}; - } - - &::before { - background-color: ${(props) => props.$headerStyle.border}; - width: ${(props) => (props.$visibleResizables ? "1px" : "0px")} !important; - } - } - } - - > thead > tr > th, - > tbody > tr > td { - border-color: ${(props) => props.$headerStyle.border}; - ${(props) => !props.$showHRowGridBorder && `border-bottom: 0px;`} - } - - td { - padding: 0px 0px; - // ${(props) => props.$showHRowGridBorder ? 'border-bottom: 1px solid #D7D9E0 !important;': `border-bottom: 0px;`} - - /* Proper styling for Fixed columns in the table body */ - &.ant-table-cell-fix-left, &.ant-table-cell-fix-right { - z-index: 1; - background: inherit; - background-color: ${(props) => props.$style.background}; - transition: background-color 0.3s; - } - - } - - /* Fix for selected and hovered rows */ - tr.ant-table-row-selected td.ant-table-cell-fix-left, - tr.ant-table-row-selected td.ant-table-cell-fix-right { - background-color: ${(props) => props.$rowStyle?.selectedRowBackground || '#e6f7ff'} !important; - } - - tr.ant-table-row:hover td.ant-table-cell-fix-left, - tr.ant-table-row:hover td.ant-table-cell-fix-right { - background-color: ${(props) => props.$rowStyle?.hoverRowBackground || '#f5f5f5'} !important; - } - - thead > tr:first-child { - th:last-child { - border-right: unset; - } - } - - tbody > tr > td:last-child { - border-right: unset !important; - } - - .ant-empty-img-simple-g { - fill: #fff; - } - - > thead > tr:first-child { - th:first-child { - border-top-left-radius: 0px; - } - - th:last-child { - border-top-right-radius: 0px; - } - } - } - - .ant-table-expanded-row-fixed:after { - border-right: unset !important; - } - } - } - - ${(props) => - props.$style && getStyle(props.$style, props.$rowStyle, props.$headerStyle, props.$toolbarStyle)} -`; - -const TableTh = styled.th<{ width?: number }>` - overflow: hidden; - > div { - overflow: hidden; - white-space: pre; - text-overflow: ellipsis; - } - - ${(props) => props.width && `width: ${props.width}px`}; -`; - -interface TableTdProps { - $background: string; - $style: TableColumnStyleType & { rowHeight?: string }; - $defaultThemeDetail: ThemeDetail; - $linkStyle?: TableColumnLinkStyleType; - $isEditing: boolean; - $tableSize?: string; - $autoHeight?: boolean; - $customAlign?: 'left' | 'center' | 'right'; -} -const TableTd = styled.td` - .ant-table-row-expand-icon, - .ant-table-row-indent { - display: ${(props) => (props.$isEditing ? "none" : "initial")}; - } - &.ant-table-row-expand-icon-cell { - background: ${(props) => props.$background}; - border-color: ${(props) => props.$style.border}; - } - background: ${(props) => props.$background} !important; - border-color: ${(props) => props.$style.border} !important; - border-radius: ${(props) => props.$style.radius}; - padding: 0 !important; - text-align: ${(props) => props.$customAlign || 'left'} !important; - - > div:not(.editing-border, .editing-wrapper), - .editing-wrapper .ant-input, - .editing-wrapper .ant-input-number, - .editing-wrapper .ant-picker { - margin: ${(props) => props.$isEditing ? '0px' : props.$style.margin}; - color: ${(props) => props.$style.text}; - font-weight: ${(props) => props.$style.textWeight}; - font-family: ${(props) => props.$style.fontFamily}; - overflow: hidden; - display: flex; - justify-content: ${(props) => props.$customAlign === 'center' ? 'center' : props.$customAlign === 'right' ? 'flex-end' : 'flex-start'}; - align-items: center; - text-align: ${(props) => props.$customAlign || 'left'}; - box-sizing: border-box; - ${(props) => props.$tableSize === 'small' && ` - padding: 1px 8px; - font-size: ${props.$defaultThemeDetail.textSize == props.$style.textSize ? '14px !important' : props.$style.textSize + ' !important'}; - font-style:${props.$style.fontStyle} !important; - min-height: ${props.$style.rowHeight || '14px'}; - line-height: 20px; - ${!props.$autoHeight && ` - overflow-y: auto; - max-height: ${props.$style.rowHeight || '28px'}; - `}; - `}; - ${(props) => props.$tableSize === 'middle' && ` - padding: 8px 8px; - font-size: ${props.$defaultThemeDetail.textSize == props.$style.textSize ? '16px !important' : props.$style.textSize + ' !important'}; - font-style:${props.$style.fontStyle} !important; - min-height: ${props.$style.rowHeight || '24px'}; - line-height: 24px; - ${!props.$autoHeight && ` - overflow-y: auto; - max-height: ${props.$style.rowHeight || '48px'}; - `}; - `}; - ${(props) => props.$tableSize === 'large' && ` - padding: 16px 16px; - font-size: ${props.$defaultThemeDetail.textSize == props.$style.textSize ? '18px !important' : props.$style.textSize + ' !important'}; - font-style:${props.$style.fontStyle} !important; - min-height: ${props.$style.rowHeight || '48px'}; - ${!props.$autoHeight && ` - overflow-y: auto; - max-height: ${props.$style.rowHeight || '96px'}; - `}; - `}; - - > .ant-badge > .ant-badge-status-text, - > div > .markdown-body { - color: ${(props) => props.$style.text}; - } - - > div > svg g { - stroke: ${(props) => props.$style.text}; - } - - // dark link|links color - > a, - > div a { - color: ${(props) => props.$linkStyle?.text}; - - &:hover { - color: ${(props) => props.$linkStyle?.hoverText}; - } - - &:active { - color: ${(props) => props.$linkStyle?.activeText}; - } - } - } -`; - -const TableTdLoading = styled(Skeleton.Button)` - width: 90% !important; - display: table !important; - - .ant-skeleton-button { - min-width: auto !important; - display: block !important; - ${(props) => props.$tableSize === 'small' && ` - height: 20px !important; - `} - ${(props) => props.$tableSize === 'middle' && ` - height: 24px !important; - `} - ${(props) => props.$tableSize === 'large' && ` - height: 28px !important; - `} - } -`; -const ResizeableTitle = (props: any) => { - const { onResize, onResizeStop, width, viewModeResizable, ...restProps } = props; - const [childWidth, setChildWidth] = useState(0); - const resizeRef = useRef(null); - const isUserViewMode = useUserViewMode(); - const updateChildWidth = useCallback(() => { - if (resizeRef.current) { - const width = resizeRef.current.getBoundingClientRect().width; - setChildWidth(width); - } - }, []); - - useEffect(() => { - updateChildWidth(); - const resizeObserver = new ResizeObserver(() => { - updateChildWidth(); - }); - - if (resizeRef.current) { - resizeObserver.observe(resizeRef.current); - } - return () => { - resizeObserver.disconnect(); - }; - }, [updateChildWidth]); - - const isNotDataColumn = _.isNil(restProps.title); - if ((isUserViewMode && !restProps.viewModeResizable) || isNotDataColumn) { - return ; - } - - return ( - 0 ? width : childWidth} - height={0} - onResize={(e: React.SyntheticEvent, { size }: { size: { width: number } }) => { - e.stopPropagation(); - onResize(size.width); - }} - onResizeStart={(e: React.SyntheticEvent) => { - updateChildWidth(); - e.stopPropagation(); - e.preventDefault(); - }} - onResizeStop={onResizeStop} - draggableOpts={{ enableUserSelectHack: false }} - handle={(axis: ResizeHandleAxis, ref: ReactRef) => ( - { - e.preventDefault(); - e.stopPropagation(); - }} - /> - )} - > - - - ); -}; -type CustomTableProps = Omit, "components" | "columns"> & { - columns: CustomColumnType[]; - viewModeResizable: boolean; - visibleResizables: boolean; - rowColorFn: RowColorViewType; - rowHeightFn: RowHeightViewType; - columnsStyle: TableColumnStyleType; - size?: string; - rowAutoHeight?: boolean; - customLoading?: boolean; - onCellClick: (columnName: string, dataIndex: string) => void; -}; -const TableCellView = React.memo((props: { - record: RecordType; - title: string; - rowColorFn: RowColorViewType; - rowHeightFn: RowHeightViewType; - cellColorFn: CellColorViewType; - rowIndex: number; - children: any; - columnsStyle: TableColumnStyleType; - columnStyle: TableColumnStyleType; - linkStyle: TableColumnLinkStyleType; - tableSize?: string; - autoHeight?: boolean; - loading?: boolean; - customAlign?: 'left' | 'center' | 'right'; -}) => { - const { - record, - title, - rowIndex, - rowColorFn, - rowHeightFn, - cellColorFn, - children, - columnsStyle, - columnStyle, - linkStyle, - tableSize, - autoHeight, - loading, - customAlign, - ...restProps - } = props; - const [editing, setEditing] = useState(false); - const rowContext = useContext(TableRowContext); - - // Memoize style calculations - const style = useMemo(() => { - if (!record) return null; - const rowColor = rowColorFn({ - currentRow: record, - currentIndex: rowIndex, - currentOriginalIndex: record[OB_ROW_ORI_INDEX], - columnTitle: title, - }); - const rowHeight = rowHeightFn({ - currentRow: record, - currentIndex: rowIndex, - currentOriginalIndex: record[OB_ROW_ORI_INDEX], - columnTitle: title, - }); - const cellColor = cellColorFn({ - currentCell: record[title], - currentRow: record, - }); - return { - background: cellColor || rowColor || columnStyle.background || columnsStyle.background, - margin: columnStyle.margin || columnsStyle.margin, - text: columnStyle.text || columnsStyle.text, - border: columnStyle.border || columnsStyle.border, - radius: columnStyle.radius || columnsStyle.radius, - // borderWidth: columnStyle.borderWidth || columnsStyle.borderWidth, - textSize: columnStyle.textSize || columnsStyle.textSize, - textWeight: columnsStyle.textWeight || columnStyle.textWeight, - fontFamily: columnsStyle.fontFamily || columnStyle.fontFamily, - fontStyle: columnsStyle.fontStyle || columnStyle.fontStyle, - rowHeight: rowHeight, - }; - }, [record, rowIndex, title, rowColorFn, rowHeightFn, cellColorFn, columnStyle, columnsStyle]); - - let tdView; - if (!record) { - tdView = {children}; - } else { - let { background } = style!; - if (rowContext.hover) { - background = 'transparent'; - } - - tdView = ( - - {loading - ? - : children - } - - ); - } - - return ( - - {tdView} - - ); -}); - -const TableRowView = React.memo((props: any) => { - const [hover, setHover] = useState(false); - const [selected, setSelected] = useState(false); - - // Memoize event handlers - const handleMouseEnter = useCallback(() => setHover(true), []); - const handleMouseLeave = useCallback(() => setHover(false), []); - const handleFocus = useCallback(() => setSelected(true), []); - const handleBlur = useCallback(() => setSelected(false), []); - - return ( - - - - ); -}); - -/** - * A table with adjustable column width, width less than 0 means auto column width - */ -function ResizeableTableComp(props: CustomTableProps) { - const { - columns, - viewModeResizable, - visibleResizables, - rowColorFn, - rowHeightFn, - columnsStyle, - size, - rowAutoHeight, - customLoading, - onCellClick, - ...restProps - } = props; - const [resizeData, setResizeData] = useState({ index: -1, width: -1 }); - - // Memoize resize handlers - const handleResize = useCallback((width: number, index: number) => { - setResizeData({ index, width }); - }, []); - - const handleResizeStop = useCallback((width: number, index: number, onWidthResize?: (width: number) => void) => { - setResizeData({ index: -1, width: -1 }); - if (onWidthResize) { - onWidthResize(width); - } - }, []); - - // Memoize cell handlers - const createCellHandler = useCallback((col: CustomColumnType) => { - return (record: RecordType, index: number) => ({ - record, - title: String(col.dataIndex), - rowColorFn, - rowHeightFn, - cellColorFn: col.cellColorFn, - rowIndex: index, - columnsStyle, - columnStyle: col.style, - linkStyle: col.linkStyle, - tableSize: size, - autoHeight: rowAutoHeight, - onClick: () => onCellClick(col.titleText, String(col.dataIndex)), - loading: customLoading, - customAlign: col.align, - }); - }, [rowColorFn, rowHeightFn, columnsStyle, size, rowAutoHeight, onCellClick, customLoading]); - - // Memoize header cell handlers - const createHeaderCellHandler = useCallback((col: CustomColumnType, index: number, resizeWidth: number) => { - return () => ({ - width: resizeWidth, - title: col.titleText, - viewModeResizable, - onResize: (width: React.SyntheticEvent) => { - if (width) { - handleResize(Number(width), index); - } - }, - onResizeStop: (e: React.SyntheticEvent, { size }: { size: { width: number } }) => { - handleResizeStop(size.width, index, col.onWidthResize); - }, - }); - }, [viewModeResizable, handleResize, handleResizeStop]); - - // Memoize columns to prevent unnecessary re-renders - const memoizedColumns = useMemo(() => { - return columns.map((col, index) => { - const { width, style, linkStyle, cellColorFn, onWidthResize, ...restCol } = col; - const resizeWidth = (resizeData.index === index ? resizeData.width : col.width) ?? 0; - - const column: ColumnType = { - ...restCol, - width: typeof resizeWidth === "number" && resizeWidth > 0 ? resizeWidth : undefined, - minWidth: typeof resizeWidth === "number" && resizeWidth > 0 ? undefined : COL_MIN_WIDTH, - onCell: (record: RecordType, index?: number) => createCellHandler(col)(record, index ?? 0), - onHeaderCell: () => createHeaderCellHandler(col, index, Number(resizeWidth))(), - }; - return column; - }); - }, [columns, resizeData, createCellHandler, createHeaderCellHandler]); - - return ( - - components={{ - header: { - cell: ResizeableTitle, - }, - body: { - cell: TableCellView, - row: TableRowView, - }, - }} - {...restProps} - pagination={false} - columns={memoizedColumns} - scroll={{ - x: COL_MIN_WIDTH * columns.length, - }} - /> - ); -} -ResizeableTableComp.whyDidYouRender = true; - -const ResizeableTable = React.memo(ResizeableTableComp) as typeof ResizeableTableComp; const createNewEmptyRow = ( @@ -848,7 +98,6 @@ export const TableCompView = React.memo((props: { const toolbarStyle = compChildren.toolbarStyle.getView(); const hideToolbar = compChildren.hideToolbar.getView() const rowAutoHeight = compChildren.rowAutoHeight.getView(); - const tableAutoHeight = comp.getTableAutoHeight(); const showHorizontalScrollbar = compChildren.showHorizontalScrollbar.getView(); const showVerticalScrollbar = compChildren.showVerticalScrollbar.getView(); const visibleResizables = compChildren.visibleResizables.getView(); @@ -872,6 +121,7 @@ export const TableCompView = React.memo((props: { const onEvent = useMemo(() => compChildren.onEvent.getView(), [compChildren.onEvent]); const currentExpandedRows = useMemo(() => compChildren.currentExpandedRows.getView(), [compChildren.currentExpandedRows]); const dynamicColumn = compChildren.dynamicColumn.getView(); + const dynamicColumnConfig = useMemo( () => compChildren.dynamicColumnConfig.getView(), [compChildren.dynamicColumnConfig] @@ -1006,6 +256,31 @@ export const TableCompView = React.memo((props: { const childrenProps = childrenToProps(comp.children); +// Table mode and height configuration + const tableMode = useTableMode(comp.getTableAutoHeight()); + const { containerHeight, containerRef } = useContainerHeight(tableMode.isFixedMode); + + const virtualizationConfig = useVirtualization( + containerHeight, + pageDataInfo.data.length, + size as 'small' | 'middle' | 'large', + { + showToolbar: !hideToolbar, + showHeader: !compChildren.hideHeader.getView(), + stickyToolbar: toolbar.fixedToolbar && toolbar.position === 'above', + isFixedMode: tableMode.isFixedMode + } + ); + const totalColumnsWidth = COL_MIN_WIDTH * antdColumns.length; + const scrollConfig = useScrollConfiguration( + virtualizationConfig.enabled, + virtualizationConfig.scrollY, + totalColumnsWidth + ); + + + + useMergeCompStyles( childrenProps as Record, comp.dispatch @@ -1082,7 +357,8 @@ export const TableCompView = React.memo((props: { ); } - const hideScrollbar = !showHorizontalScrollbar && !showVerticalScrollbar; + const hideScrollbar = (!showHorizontalScrollbar && !showVerticalScrollbar) || + (scrollConfig.virtual && (showHorizontalScrollbar || showVerticalScrollbar)); const showTableLoading = loading || // fixme isLoading type ((showDataLoadingIndicators) && @@ -1092,20 +368,20 @@ export const TableCompView = React.memo((props: { return ( - {toolbar.position === "above" && !hideToolbar && (toolbar.fixedToolbar || (tableAutoHeight && showHorizontalScrollbar)) && toolbarView} + {toolbar.position === "above" && !hideToolbar && (toolbar.fixedToolbar || (tableMode.isAutoMode && showHorizontalScrollbar)) && toolbarView} expandable={{ @@ -1162,13 +441,15 @@ export const TableCompView = React.memo((props: { }); }} summary={summaryView} + scroll={scrollConfig.scroll} + virtual={scrollConfig.virtual} /> {expansion.expandModalView} - {toolbar.position === "below" && !hideToolbar && (toolbar.fixedToolbar || (tableAutoHeight && showHorizontalScrollbar)) && toolbarView} + {toolbar.position === "below" && !hideToolbar && (toolbar.fixedToolbar || (tableMode.isAutoMode && showHorizontalScrollbar)) && toolbarView} diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/tableStyles.ts b/client/packages/lowcoder/src/comps/comps/tableComp/tableStyles.ts new file mode 100644 index 000000000..6dd1a770f --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/tableComp/tableStyles.ts @@ -0,0 +1,339 @@ +import styled, { css } from "styled-components"; +import { isValidColor, darkenColor } from "lowcoder-design"; +import { PrimaryColor } from "constants/style"; +import { defaultTheme } from "@lowcoder-ee/constants/themeConstants"; +import { TableStyleType, TableRowStyleType, TableHeaderStyleType, TableToolbarStyleType } from "comps/controls/styleControlConstants"; +import { getVerticalMargin } from "@lowcoder-ee/util/cssUtil"; + +export function genLinerGradient(color: string) { + return isValidColor(color) ? `linear-gradient(${color}, ${color})` : color; +} + +export const getStyle = ( + style: TableStyleType, + rowStyle: TableRowStyleType, + headerStyle: TableHeaderStyleType, + toolbarStyle: TableToolbarStyleType, +) => { + const background = genLinerGradient(style.background); + const selectedRowBackground = genLinerGradient(rowStyle.selectedRowBackground); + const hoverRowBackground = genLinerGradient(rowStyle.hoverRowBackground); + const alternateBackground = genLinerGradient(rowStyle.alternateBackground); + + return css` + .ant-table-body { + background: ${genLinerGradient(style.background)}; + } + .ant-table-tbody { + > tr:nth-of-type(2n + 1) { + background: ${genLinerGradient(rowStyle.background)}; + } + + > tr:nth-of-type(2n) { + background: ${alternateBackground}; + } + + // selected row + > tr:nth-of-type(2n + 1).ant-table-row-selected { + background: ${selectedRowBackground}, ${rowStyle.background} !important; + > td.ant-table-cell { + background: transparent !important; + } + + // > td.ant-table-cell-row-hover, + &:hover { + background: ${hoverRowBackground}, ${selectedRowBackground}, ${rowStyle.background} !important; + } + } + + > tr:nth-of-type(2n).ant-table-row-selected { + background: ${selectedRowBackground}, ${alternateBackground} !important; + > td.ant-table-cell { + background: transparent !important; + } + + // > td.ant-table-cell-row-hover, + &:hover { + background: ${hoverRowBackground}, ${selectedRowBackground}, ${alternateBackground} !important; + } + } + + // hover row + > tr:nth-of-type(2n + 1):hover { + background: ${hoverRowBackground}, ${rowStyle.background} !important; + > td.ant-table-cell-row-hover { + background: transparent; + } + } + > tr:nth-of-type(2n):hover { + background: ${hoverRowBackground}, ${alternateBackground} !important; + > td.ant-table-cell-row-hover { + background: transparent; + } + } + + > tr.ant-table-expanded-row { + background: ${background}; + } + } + `; +}; + +export const BackgroundWrapper = styled.div<{ + $style: TableStyleType; + $tableAutoHeight: boolean; + $showHorizontalScrollbar: boolean; + $showVerticalScrollbar: boolean; + $fixedToolbar: boolean; +}>` + display: flex; + flex-direction: column; + background: ${(props) => props.$style.background} !important; + border-radius: ${(props) => props.$style.radius} !important; + padding: ${(props) => props.$style.padding} !important; + margin: ${(props) => props.$style.margin} !important; + border-style: ${(props) => props.$style.borderStyle} !important; + border-width: ${(props) => `${props.$style.borderWidth} !important`}; + border-color: ${(props) => `${props.$style.border} !important`}; + height: calc(100% - ${(props) => props.$style.margin && getVerticalMargin(props.$style.margin.split(' '))}); + overflow: hidden; + + > div.table-scrollbar-wrapper { + overflow: auto; + ${(props) => props.$fixedToolbar && `height: auto`}; + + ${(props) => (props.$showHorizontalScrollbar || props.$showVerticalScrollbar) && ` + .simplebar-content-wrapper { + overflow: auto !important; + } + `} + + ${(props) => !props.$showHorizontalScrollbar && ` + div.simplebar-horizontal { + visibility: hidden !important; + } + `} + ${(props) => !props.$showVerticalScrollbar && ` + div.simplebar-vertical { + visibility: hidden !important; + } + `} + } +`; + +// TODO: find a way to limit the calc function for max-height only to first Margin value +export const TableWrapper = styled.div<{ + $style: TableStyleType; + $headerStyle: TableHeaderStyleType; + $toolbarStyle: TableToolbarStyleType; + $rowStyle: TableRowStyleType; + $toolbarPosition: "above" | "below" | "close"; + $fixedHeader: boolean; + $fixedToolbar: boolean; + $visibleResizables: boolean; + $showHRowGridBorder?: boolean; + $isVirtual?: boolean; + $showHorizontalScrollbar?: boolean; + $showVerticalScrollbar?: boolean; +}>` + .ant-table-wrapper { + border-top: unset; + border-color: inherit; + } + + .ant-table-row-expand-icon { + color: ${PrimaryColor}; + } + + .ant-table .ant-table-cell-with-append .ant-table-row-expand-icon { + margin: 0; + top: 18px; + left: 4px; + } + + .ant-table.ant-table-small .ant-table-cell-with-append .ant-table-row-expand-icon { + top: 10px; + } + + .ant-table.ant-table-middle .ant-table-cell-with-append .ant-table-row-expand-icon { + top: 14px; + margin-right:5px; + } + + .ant-table { + background: ${(props) =>props.$style.background}; + .ant-table-container { + border-left: unset; + border-top: none !important; + border-inline-start: none !important; + + &::after { + box-shadow: none !important; + } + + .ant-table-content { + overflow: unset !important + } + + // A table expand row contains table + .ant-table-tbody .ant-table-wrapper:only-child .ant-table { + margin: 0; + } + + table { + border-top: unset; + + > .ant-table-thead { + ${(props) => + props.$fixedHeader && ` + position: sticky; + position: -webkit-sticky; + // top: ${props.$fixedToolbar ? '47px' : '0'}; + top: 0; + z-index: 2; + ` + } + > tr { + background: ${(props) => props.$headerStyle.headerBackground}; + } + > tr > th { + background: transparent; + border-color: ${(props) => props.$headerStyle.border}; + border-width: ${(props) => props.$headerStyle.borderWidth}; + color: ${(props) => props.$headerStyle.headerText}; + // border-inline-end: ${(props) => `${props.$headerStyle.borderWidth} solid ${props.$headerStyle.border}`} !important; + + /* Proper styling for fixed header cells */ + &.ant-table-cell-fix-left, &.ant-table-cell-fix-right { + z-index: 1; + background: ${(props) => props.$headerStyle.headerBackground}; + } + + + } + + + > tr > th { + + > div { + margin: ${(props) => props.$headerStyle.margin}; + + &, .ant-table-column-title > div { + font-size: ${(props) => props.$headerStyle.textSize}; + font-weight: ${(props) => props.$headerStyle.textWeight}; + font-family: ${(props) => props.$headerStyle.fontFamily}; + font-style: ${(props) => props.$headerStyle.fontStyle}; + color:${(props) => props.$headerStyle.headerText} + } + } + + &:last-child { + border-inline-end: none !important; + } + &.ant-table-column-has-sorters:hover { + background-color: ${(props) => darkenColor(props.$headerStyle.headerBackground, 0.05)}; + } + + > .ant-table-column-sorters > .ant-table-column-sorter { + color: ${(props) => props.$headerStyle.headerText === defaultTheme.textDark ? "#bfbfbf" : props.$headerStyle.headerText}; + } + + &::before { + background-color: ${(props) => props.$headerStyle.border}; + width: ${(props) => (props.$visibleResizables ? "1px" : "0px")} !important; + } + } + } + + > thead > tr > th, + > tbody > tr > td { + border-color: ${(props) => props.$headerStyle.border}; + ${(props) => !props.$showHRowGridBorder && `border-bottom: 0px;`} + } + + td { + padding: 0px 0px; + // ${(props) => props.$showHRowGridBorder ? 'border-bottom: 1px solid #D7D9E0 !important;': `border-bottom: 0px;`} + + /* Proper styling for Fixed columns in the table body */ + &.ant-table-cell-fix-left, &.ant-table-cell-fix-right { + z-index: 1; + background: inherit; + background-color: ${(props) => props.$style.background}; + transition: background-color 0.3s; + } + + } + + /* Fix for selected and hovered rows */ + tr.ant-table-row-selected td.ant-table-cell-fix-left, + tr.ant-table-row-selected td.ant-table-cell-fix-right { + background-color: ${(props) => props.$rowStyle?.selectedRowBackground || '#e6f7ff'} !important; + } + + tr.ant-table-row:hover td.ant-table-cell-fix-left, + tr.ant-table-row:hover td.ant-table-cell-fix-right { + background-color: ${(props) => props.$rowStyle?.hoverRowBackground || '#f5f5f5'} !important; + } + + thead > tr:first-child { + th:last-child { + border-right: unset; + } + } + + tbody > tr > td:last-child { + border-right: unset !important; + } + + .ant-empty-img-simple-g { + fill: #fff; + } + + > thead > tr:first-child { + th:first-child { + border-top-left-radius: 0px; + } + + th:last-child { + border-top-right-radius: 0px; + } + } + } + + .ant-table-expanded-row-fixed:after { + border-right: unset !important; + } + } + } + + // ANTD Virtual Scrollbar Styling + .ant-table-tbody-virtual-scrollbar-vertical { + .ant-table-tbody-virtual-scrollbar-thumb { + display: ${(props) => (props.$isVirtual && props.$showVerticalScrollbar) ? 'block' : 'none'} !important; + background: rgba(0, 0, 0, 0.3) !important; + border-radius: 3px !important; + cursor: pointer !important; + + &:hover { + background: rgba(0, 0, 0, 0.5) !important; + } + } + } + + .ant-table-tbody-virtual-scrollbar-horizontal { + .ant-table-tbody-virtual-scrollbar-thumb { + display: ${(props) => (props.$isVirtual && props.$showHorizontalScrollbar) ? 'block' : 'none'} !important; + background: rgba(0, 0, 0, 0.3) !important; + border-radius: 3px !important; + cursor: pointer !important; + + &:hover { + background: rgba(0, 0, 0, 0.5) !important; + } + } + } + + ${(props) => + props.$style && getStyle(props.$style, props.$rowStyle, props.$headerStyle, props.$toolbarStyle)} +`; diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/tableUtils.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/tableUtils.tsx index 025e91e51..b3fe78d04 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/tableUtils.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/tableUtils.tsx @@ -30,6 +30,26 @@ export const OB_ROW_RECORD = "__ob_origin_record"; export const COL_MIN_WIDTH = 55; export const COL_MAX_WIDTH = 500; +/* + +======================== Virtualization constants ========================= + +*/ +export const VIRTUAL_ROW_HEIGHTS = { + small: 32, + middle: 48, + large: 80 +} as const; + + +export const VIRTUAL_THRESHOLD = 50; +export const MIN_VIRTUAL_HEIGHT = 200; // Minimum container height needed for virtualization +export const TOOLBAR_HEIGHT = 48; // Standard toolbar height +export const HEADER_HEIGHT = 40; // Standard header height + + /* ========================== End of Virtualization constants ========================== */ + + /** * Add __originIndex__, mainly for the logic of the default key */