Commit 14a68e30 by zhaochengxiang

模型增加虚拟滚动

parent e4fb0d89
......@@ -30,6 +30,8 @@
"local-storage": "^2.0.0",
"react": "^17.0.1",
"react-contexify": "^5.0.0",
"react-contextmenu": "^2.14.0",
"react-data-grid": "^7.0.0-beta.12",
"react-dnd": "^14.0.2",
"react-dnd-html5-backend": "^14.0.0",
"react-dom": "^17.0.1",
......
import React, { useState, useEffect, useRef } from "react";
import React, { useState, useEffect, useRef, useMemo } from "react";
import { Tooltip, Modal, Pagination, Table, Typography } from 'antd';
import { DownOutlined, UpOutlined } from '@ant-design/icons';
import SmoothScroll from 'smooth-scroll';
......@@ -7,6 +7,7 @@ import { Resizable } from 'react-resizable';
import { useContextMenu, Menu as RcMenu, Item as RcItem } from "react-contexify";
import ResizeObserver from 'rc-resize-observer';
import DataGrid from '../../VirtualTable/test-table';
import { dispatch } from '../../../../model';
import { showMessage, getQueryParam, paginate, isSzseEnv, formatDate, getDataModelerRole } from '../../../../util';
import { AnchorId, AnchorTimestamp, Action, CatalogId, ModelerId, DataModelerRoleReader } from '../../../../util/constant';
......@@ -94,36 +95,6 @@ const ModelNameColumn = (props) => {
);
}
const ResizeableHeaderCell = props => {
const { onResize, width, onClick, ...restProps } = props;
if (!width) {
return <th {...restProps} />;
}
return (
<Resizable
width={width}
height={0}
handle={
<span
className="react-resizable-handle"
onClick={(e) => {
e.stopPropagation();
}}
/>
}
onResize={onResize}
draggableOpts={{ enableUserSelectHack: false }}
>
<th
onClick={onClick}
{...restProps}
/>
</Resizable>
);
};
const ModelTable = (props) => {
const { data, onChange, onItemAction, onSelect, onHistory, catalogId, keyword, onAutoCreateTable, offset = null, modelId = null, modelPid = null, view, selectModelerIds, onSubSelect, modelState, user } = props;
......@@ -137,144 +108,111 @@ const ModelTable = (props) => {
const [ tableWidth, setTableWidth ] = useState(0);
const [ selectedRowKeys, setSelectedRowKeys ] = useState([]);
const [ subSelectedRowKeys, setSubSelectedRowKeys ] = useState([]);
// const [ mouseEnterKey, setMouseEnterKey ] = useState(null);
const [ sortRule, setSortRule ] = useState(null);
const [ filterData, setFilterData ] = useState([]);
const [ subData, setSubData ] = useState([]);
const cols = [
// {
// name: '序号',
// key: 'key',
// width: 60,
// sortable: false,
// },
{
title: '序号',
dataIndex: 'key',
render: (text, record, index) => {
return (index+1).toString();
},
width: 60,
ellipsis: true,
},
{
title: '模型名称',
dataIndex: 'name',
name: '模型名称',
key: 'name',
width: isSzseEnv?360:160,
ellipsis: true,
sorter: true,
sortDirections: ['ascend', 'descend'],
render: (text, record, index) => {
return (<ModelNameColumn text={text} record={record} detailItem={detailItem} />);
sortable: true,
resizable: true,
formatter(props) {
return (<ModelNameColumn text={props.row.name} record={props.row} detailItem={detailItem} />);
}
},
{
title: '中文名称',
dataIndex: 'cnName',
name: '中文名称',
key: 'cnName',
width: isSzseEnv?420:160,
ellipsis: true,
sorter: true,
sortDirections: ['ascend', 'descend'],
render: (text, _, __) => {
sortable: true,
resizable: true,
formatter(props) {
return (
<Tooltip title={text||''}>
<Text ellipsis={true}>{text||''}</Text>
<Tooltip title={props.row.cnName||''}>
<Text ellipsis={true}>{props.row.cnName||''}</Text>
</Tooltip>
)
}
},
{
title: '状态',
dataIndex: 'state',
name: '状态',
key: 'state',
width: 100,
ellipsis: true,
sorter: true,
sortDirections: ['ascend', 'descend'],
render: (_, record) => {
sortable: true,
resizable: true,
formatter(props) {
let color = '';
if (record?.state?.id === '1') {
if (props.row.state?.id === '1') {
color = '#DE7777';
} else if (record?.state?.id === '2') {
} else if (props.row.state?.id === '2') {
color = '#779BDE';
} else if (record?.state?.id === '4') {
} else if (props.row.state?.id === '4') {
color = '#77DEBF';
}
return (
<span>
<span style={{ display: 'inline-block', width: 10, height: 10, borderRadius: 5, marginRight: 5, backgroundColor: color }}></span>
<span>{record?.state?.cnName||''}</span>
<span>{props.row.state?.cnName||''}</span>
</span>
);
}
},
{
title: '创建人',
dataIndex: 'editor',
name: '创建人',
key: 'editor',
width: 100,
ellipsis: true,
sorter: true,
sortDirections: ['ascend', 'descend'],
sortable: true,
resizable: true,
},
{
title: '版本号',
dataIndex: 'modifiedTs',
name: '版本号',
key: 'modifiedTs',
width: 170,
ellipsis: true,
sorter: true,
sortDirections: ['ascend', 'descend'],
render: (_,record) => {
return `V_${formatDate(record.modifiedTs)}`;
sortable: true,
resizable: true,
formatter(props) {
return `V_${formatDate(props.row.modifiedTs)}`;
}
},
// {
// title: '标签',
// dataIndex: 'tag',
// width: 200,
// onCell: (record) => ({
// onMouseEnter: event => {
// setMouseEnterKey(record.id);
// },
// onMouseLeave: event => {
// setMouseEnterKey(null);
// },
// }),
// render: (_,record) => {
// return (
// record.id===mouseEnterKey?<Tag styleType='complex' id={record.id} />:<Tag id={record.id} />
// );
// }
// },
{
title: '模型描述',
dataIndex: 'remark',
ellipsis: true,
sorter: true,
sortDirections: ['ascend', 'descend'],
render: (text, _, __) => {
name: '模型描述',
key: 'remark',
sortable: true,
resizable: true,
formatter(props) {
return (
<Tooltip title={text||''} overlayClassName='tooltip-common'>
<Text ellipsis={true}>{text||''}</Text>
<Tooltip title={props.row.remark||''} overlayClassName='tooltip-common'>
<Text ellipsis={true}>{props.row.remark||''}</Text>
</Tooltip>
);
)
}
},
];
const pathColumn = {
title: '路径',
dataIndex: 'path',
name: '路径',
key: 'path',
width: 120,
ellipsis: true,
sorter: true,
sortDirections: ['ascend', 'descend'],
render: (text, _, __) => {
sortable: true,
resizable: true,
formatter(props) {
return (
<Tooltip title={text||''}>
<Text ellipsis={true}>{text||''}</Text>
<Tooltip title={props.row.path||''}>
<Text ellipsis={true}>{props.row.path||''}</Text>
</Tooltip>
)
}
};
const [ columns, setColumns ] = useState([]);
const [ includePathColumns, setIncludePathColumns ] = useState([]);
const [ pagination, setPagination ] = useState( { pageNum: 1, pageSize: 20 } );
const [ currentItem, setCurrentItem ] = useState(null);
const { pageNum, pageSize } = pagination;
......@@ -286,6 +224,17 @@ const ModelTable = (props) => {
const shouldScrollRef = useRef(false);
const columns = useMemo(() => {
const newColumns = [...cols];
if (view==='state' || (keyword||'')!=='') {
newColumns.splice(3, 0, pathColumn);
}
return newColumns;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [view, keyword]);
useEffect(() => {
if ((modelId||'') !== '') {
......@@ -351,89 +300,6 @@ const ModelTable = (props) => {
}
})
useEffect(() => {
const newData = [...data];
if (sortRule) {
if (sortRule.order === 'ascend') {
newData.sort((item1, item2) => {
if (sortRule.field === 'state') {
return (item1[sortRule.field]?.cnName||'').localeCompare(item2[sortRule.field]?.cnName||'');
} else if (sortRule.field === 'modifiedTs') {
return formatDate(item1[sortRule.field]).localeCompare(formatDate(item2[sortRule.field]));
}
return item1[sortRule.field].localeCompare(item2[sortRule.field]);
})
} else if (sortRule.order === 'descend') {
newData.sort((item1, item2) => {
if (sortRule.field === 'state') {
return (item2[sortRule.field]?.cnName||'').localeCompare(item1[sortRule.field]?.cnName||'');
} else if (sortRule.field === 'modifiedTs') {
return formatDate(item2[sortRule.field]).localeCompare(formatDate(item1[sortRule.field]));
}
return item2[sortRule.field].localeCompare(item1[sortRule.field]);
})
}
}
const _data = paginate(newData||[], pageNum, pageSize);
setFilterData(_data);
//eslint-disable-next-line react-hooks/exhaustive-deps
}, [data, pagination, sortRule])
useEffect(() => {
if (tableWidth>0 && columns.length===0) {
let newColumns = [], newIncludePathColumns = [];
let excludePathCols = [...cols];
if ((modelId||'') !== '') {
excludePathCols = cols.filter(item => item.dataIndex!=='key');
}
excludePathCols.forEach((column, index) => {
const newColumn = {...column};
if (!newColumn.width) {
const rowWidth = (excludePathCols.reduce((preVal, col) => (col.width?col.width:0) + preVal, 0)) + 97; //展开50 勾选32 滚动条15
if (tableWidth - rowWidth > 200) {
newColumn.width = tableWidth - rowWidth;
} else {
newColumn.width = 200;
}
}
newColumns.push(newColumn);
});
const includePathCols = [...cols];
includePathCols.splice(3, 0, pathColumn);
includePathCols.forEach((column, index) => {
const newColumn = {...column};
if (!newColumn.width) {
const rowWidth = (includePathCols.reduce((preVal, col) => (col.width?col.width:0) + preVal, 0)) + 97;
if (tableWidth - rowWidth > 200) {
newColumn.width = tableWidth-rowWidth;
} else {
newColumn.width = 200;
}
}
newIncludePathColumns.push(newColumn);
});
setColumns([ ...newColumns, <Column key='auto' />]);
setIncludePathColumns([ ...newIncludePathColumns, <Column key='auto' />]);
}
//eslint-disable-next-line react-hooks/exhaustive-deps
}, [ tableWidth ])
const modelEventChange = (e) => {
if (e.key === 'modelChange') {
getCheckoutDataModel();
......@@ -580,37 +446,6 @@ const ModelTable = (props) => {
}
}
const handleResize = index => (e, { size }) => {
let nextColumns = [...columns];
if ((modelId||'')==='' && (view==='state'||(keyword||'')!=='')) {
nextColumns = [...includePathColumns];
}
nextColumns[index] = {
...nextColumns[index],
width: size.width,
};
if ((modelId||'')==='' && (view==='state'||(keyword||'')!=='')) {
setIncludePathColumns(nextColumns);
} else {
setColumns(nextColumns);
}
};
const onTableChange = (pagination, filters, sorter, extra) => {
if (sorter) {
setSortRule(sorter);
}
}
const rowSelection = {
selectedRowKeys,
onChange: onSelectChange,
hideSelectAll: (modelId||'') !=='',
};
const classes = classnames('model-table', {
'model-table-sub': modelId
});
......@@ -678,23 +513,6 @@ const ModelTable = (props) => {
}
}
const mergedColumns = () => {
let newColumns = [...columns];
if ((modelId||'')==='' && (view==='state'||(keyword||'')!=='')) {
newColumns = [...includePathColumns];
}
return (
newColumns.map((col, index) => ({
...col,
onHeaderCell: column => ({
width: column.width,
onResize: handleResize(index),
}),
}))
);
}
let disableEdit = false, disableDelete = false, editTip = '', deleteTip = '', editMenuTitle = '编辑';
if (!currentItem?.editable && currentItem?.state?.id!=='4') {
......@@ -723,57 +541,26 @@ const ModelTable = (props) => {
return (
<div className={classes}>
<ResizeObserver
onResize={({ width }) => {
setTableWidth(width);
}}
>
<Table
rowSelection={rowSelection}
components={{
header: {
cell: ResizeableHeaderCell,
}
}}
columns={mergedColumns()}
rowKey={'id'}
dataSource={modelId?(subData||[]):(filterData||[])}
pagination={false}
size={modelId?'small':'default'}
onRow={(record, index) => {
return {
id: `data-model-${record?.id}`,
style: { backgroundColor: (record?.id===anchorId)?'#e7f7ff':'transparent' },
onContextMenu: event => {
setCurrentItem(record);
displayMenu(event);
},
}
}}
scroll={{ y: modelId?null:((filterData||[]).length===0?null:'calc(100vh - 121px - 57px - 24px - 38px - 44px)') }}
onChange={onTableChange}
expandable={expandable}
<DataGrid
style={{ blockSize: modelId?'95px':'calc(100vh - 94px - 37px - 57px - 24px)' }}
columns={columns}
rows={modelId?(subData||[]):(data||[])}
expandRow={(_pid, row) => {
if (!row?.alreadyCheckedOut) return null;
return (
<div style={{ padding: 10 }}>
<ModelTable
modelId={row?.checkedOutId}
modelPid={row?.id}
onSubSelect={onSubSelectChange}
{...props}
/>
</ResizeObserver>
{
!modelId && (data||[]).length>0 && <Pagination
className="text-center mt-3"
showSizeChanger
showQuickJumper
onChange={(_pageNum, _pageSize) => {
setPagination({ pageNum: _pageNum, pageSize: _pageSize || 20 });
}}
onShowSizeChange={(_pageNum, _pageSize) => {
setPagination({ pageNum: 1, pageSize: _pageSize });
</div>
)
}}
current={pageNum}
pageSize={pageSize}
defaultCurrent={1}
total={(data||[]).length}
pageSizeOptions={[10,20,50]}
showTotal={total => `共 ${total} 条`}
getComparator={getComparator}
/>
}
<RcMenu id={MENU_ID}>
{
(getDataModelerRole(user)!==DataModelerRoleReader) && <RcItem id="edit" disabled={disableEdit} onClick={handleItemClick}>
......@@ -819,3 +606,26 @@ const ModelTable = (props) => {
}
export default ModelTable;
function getComparator(sortColumn) {
switch (sortColumn) {
case 'name':
case 'cnName':
case 'editor':
case 'remark':
case 'path':
return (a, b) => {
return a[sortColumn].localeCompare(b[sortColumn]);
};
case 'state':
return (a, b) => {
return a[sortColumn].id.localeCompare(b[sortColumn].id);
};
case 'modifiedTs':
return (a, b) => {
return a[sortColumn] - b[sortColumn];
};
default:
throw new Error(`unsupported sortColumn: "${sortColumn}"`);
}
}
import CaretDownOutlined from '@ant-design/icons/CaretDownOutlined';
import CaretUpOutlined from '@ant-design/icons/CaretUpOutlined';
import classNames from 'classnames'
const prefixCls = 'yy'
export const upNode = (active:boolean) => (
<CaretUpOutlined
className={classNames(`${prefixCls}-table-column-sorter-up anticon-caret-up anticon`, {
active,
})}
/>
);
export const downNode = (active:boolean) => (
<CaretDownOutlined
className={classNames(`${prefixCls}-table-column-sorter-down anticon-caret-down anticon`, {
active,
})}
/>
);
.virtual-table {
.react-contextmenu-wrapper {
display: contents;
}
.contextMenu {
border: 1px solid black;
.react-contextmenu {
background-color: #fff;
background-clip: padding-box;
border: 1px solid rgba(0, 0, 0, 0.15);
border-radius: 0.25rem;
color: #373a3c;
font-size: 16px;
margin-block-start: 2px;
margin-block-end: 0;
margin-inline-start: 0;
margin-inline-end: 0;
min-inline-size: 160px;
outline: none;
opacity: 0;
padding-block: 5px;
padding-inline: 0;
pointer-events: none;
text-align: start;
transition: opacity 250ms ease !important;
}
.react-contextmenu.react-contextmenu--visible {
opacity: 1;
pointer-events: auto;
}
.react-contextmenu-item {
background: 0 0;
border: 0;
color: #373a3c;
cursor: pointer;
font-weight: 400;
line-height: 1.5;
padding-block: 3px;
padding-inline: 20px;
text-align: inherit;
white-space: nowrap;
}
.react-contextmenu-item.react-contextmenu-item--active,
.react-contextmenu-item.react-contextmenu-item--selected {
color: #fff;
background-color: #20a0ff;
border-color: #20a0ff;
text-decoration: none;
}
.react-contextmenu-item.react-contextmenu-item--disabled,
.react-contextmenu-item.react-contextmenu-item--disabled:hover {
background-color: transparent;
border-color: rgba(0, 0, 0, 0.15);
color: #878a8c;
}
.react-contextmenu-item--divider {
border-block-end: 1px solid rgba(0, 0, 0, 0.15);
cursor: inherit;
margin-block-end: 3px;
padding-block: 2px;
padding-inline: 0;
}
.react-contextmenu-item--divider:hover {
background-color: transparent;
border-color: rgba(0, 0, 0, 0.15);
}
.react-contextmenu-item.react-contextmenu-submenu {
padding: 0;
}
.react-contextmenu-item.react-contextmenu-submenu > .react-contextmenu-item::after {
content: '▶';
display: inline-block;
position: absolute;
inset-inline-end: 7px;
}
}
.loadMoreRowsClassname {
inline-size: 180px;
padding-block: 8px;
padding-inline: 16px;
position: absolute;
inset-block-end: 8px;
inset-inline-end: 8px;
color: white;
line-height: 35px;
background: rgb(0 0 0 / 0.6);
}
}
\ No newline at end of file
import React, { ReactElement, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { ContextMenu, ContextMenuTrigger } from 'react-contextmenu';
import DataGrid, { Column, DataGridProps, Row as GridRow, RowsChangeData, SelectCellFormatter, SortColumn, SortIconProps } from 'react-data-grid';
import type { RowRendererProps, DataGridHandle } from 'react-data-grid';
import classNames from 'classnames';
import { nanoid } from 'nanoid';
import { downNode, upNode } from './test-table-helper';
import './test-table.less';
export enum RowAction {
None, Expand, Select
}
export enum RowType {
None, Detail
}
export interface RowData {
id: string,
__pid__?: string
__row__?: any
__type__?: RowType
__expanded__?: boolean
__selected__?: boolean
__action__?: RowAction
}
interface Props<Row> {
contextMenu?: {
id: string
menu: ReactElement
}
gridRef?: React.RefObject<DataGridHandle>
ctxRef?: React.RefObject<{
getSelected?: () => string[]
}>
// setRows?: (rows: any) => void
expandRow?: (pid: string, row: Row) => React.ReactElement
getComparator?: (sortColumn: string) => (a: Row, b: Row) => number
loadMoreRows?: (length: number) => Promise<Row[]> | undefined
}
function RowRenderer<Row, SR>(id: string) {
return (props: RowRendererProps<Row, SR>) => (
<ContextMenuTrigger id={id} collect={() => ({ rowIdx: props.rowIdx })}>
<GridRow {...props} />
</ContextMenuTrigger>
);
}
export function getExpandingCol<Row extends RowData, SR>({ colSpan, expand }: {
colSpan: () => number
expand?: (pid: string, row: Row) => React.ReactElement
}) {
const col: Column<Row, SR> = {
key: 'expanded',
name: '',
minWidth: 30,
width: 30,
colSpan(args) {
return args.type === 'ROW' && args.row.__type__ === RowType.Detail ? colSpan() : undefined;
},
cellClass(row) {
return row.__type__ === RowType.Detail ? 'detail' : undefined;
},
formatter({ row, isCellSelected, onRowChange }) {
if (row.__type__ === RowType.Detail) {
if (expand) {
return expand(row.__pid__!, row.__row__)
}
return (
<div>{row.__pid__}</div>
);
}
// 展开收起
return (
<div style={{ cursor: 'pointer', color: isCellSelected ? '#777' : '#ccc' }}
onClick={() => {
onRowChange({
...row,
__expanded__: !row.__expanded__,
__action__: RowAction.Expand
});
}}
>
{row.__expanded__ ? '\u25BC' : '\u25B6'}
</div>
);
}
}
return col
}
export function getSelectCol<Row extends RowData, SR>(selectAll: () => void, checkAll: boolean) {
const col: Column<Row, SR> = {
key: 'select',
name: '',
headerRenderer({ isCellSelected }) {
return (
<SelectCellFormatter
value={checkAll}
onChange={selectAll}
isCellSelected={isCellSelected}
/>
)
},
width: 80,
formatter({ row, onRowChange, isCellSelected }) {
// debug
// return <b>{JSON.stringify({ isCellSelected, selected: !!row.__selected__ })}</b>
// 选中
return (
<SelectCellFormatter
value={!!row.__selected__}
onChange={() => {
onRowChange({
...row,
__selected__: !row.__selected__,
__action__: RowAction.Select
});
}}
isCellSelected={isCellSelected}
/>
);
},
}
return col
}
function FC<Row extends RowData, SR, K extends React.Key = React.Key>(props: DataGridProps<Row, SR, K> & Props<Row>) {
const {
gridRef, ctxRef,
expandRow,
getComparator,
loadMoreRows,
contextMenu, columns, rows, ...rest } = props
const rowKeyGetter = (row: Row): K => {
return row.id as K;
}
const [checkAll, setCheckAll] = useState(false)
// 初始化onRowsChange
const onRowsChange = useCallback((rows: RowData[], { indexes }: RowsChangeData<Row, SR>) => {
const row = rows[indexes[0]];
if (row.__action__ === RowAction.Expand) {
if (!row.__expanded__) {
rows.splice(indexes[0] + 1, 1); // 删除下一行
} else {
rows.splice(indexes[0] + 1, 0, { // 插入下一行
id: nanoid(), __type__: RowType.Detail, __pid__: row.id, __row__: row,
});
}
} else if (row.__action__ === RowAction.Select) {
// nothing to do, just update
}
_setRows(rows as Row[]);
}, [])
// 已排序行
const [sortColumns, setSortColumns] = useState<readonly SortColumn[]>([]); // 排序列图标状态
const [_rows, _setRows] = useState<readonly Row[]>([])
useEffect(() => {
if (sortColumns.length === 0) {
_setRows(rows);
} else {
_setRows([...rows]
// .filter(item => item.__type__ !== RowType.Detail)
.sort((a, b) => {
for (const sort of sortColumns) {
const comparator = getComparator ? getComparator(sort.columnKey) : GetComparator(sort.columnKey);
const compResult = comparator(a, b);
if (compResult !== 0) {
return sort.direction === 'ASC' ? compResult : -compResult;
}
}
return 0;
}));
setCheckAll(false)
}
}, [rows, sortColumns, getComparator]);
// 组装功能s列
const cols = useMemo(() => {
if (expandRow) {
const cols: readonly Column<Row, SR>[] = [
getExpandingCol<Row, SR>({
colSpan: () => cols.length, expand: expandRow
}),
getSelectCol(() => { // 全选
const rows = []
for (const row of _rows) {
const _row = { ...row, __selected__: !checkAll }
rows.push(_row)
}
_setRows(rows)
setCheckAll(!checkAll)
}, checkAll),
...columns,
]
return cols
}
return columns
}, [columns, expandRow, _rows, checkAll, setCheckAll])
// 取得选中
const getSelected = useCallback(() => {
const selected = _rows.filter((item) => item.__selected__ === true).map(item => item.id)
console.debug('selected', selected, selected.length)
return selected
}, [_rows])
if (ctxRef?.current) {
ctxRef.current.getSelected = getSelected
}
// 处理滚动
const handleScroll = useCallback((event: React.UIEvent<HTMLDivElement>) => {
if (!isAtBottom(event)) return;
loadMoreRows?.(rows.length)
}, [loadMoreRows, rows])
const contextMenuId = contextMenu?.id ?? nanoid()
return (
<>
<DataGrid
className='virtual-table'
ref={gridRef}
{...rest}
columns={cols}
rows={_rows}
rowKeyGetter={rowKeyGetter}
components={{
sortIcon: SortIcon,
rowRenderer: contextMenu ? RowRenderer<Row, SR>(contextMenuId) : undefined
}}
onRowsChange={onRowsChange}
// headerRowHeight={45}
rowHeight={(args) => (args.type === 'ROW' && args.row.__type__ === RowType.Detail ? 300 : 45)}
// defaultColumnOptions={{
// sortable: true,
// resizable: true
// }}
// 排序
sortColumns={sortColumns}
onSortColumnsChange={setSortColumns}
// 滚动
onScroll={loadMoreRows ? handleScroll : undefined}
/>
{contextMenu && createPortal(
<div className='contextMenu'>
<ContextMenu id={contextMenuId} rtl={false}>
{contextMenu.menu}
</ContextMenu>
</div>,
document.body
)}
</>
);
}
export default FC
function GetComparator(sortColumn: string): (a: any, b: any) => number {
return (a, b) => {
const aVal = a[sortColumn], bVal = b[sortColumn]
if (aVal !== undefined && bVal !== undefined)
return aVal.localeCompare(bVal);
return 0
};
}
function isAtBottom({ currentTarget }: React.UIEvent<HTMLDivElement>): boolean {
return currentTarget.scrollTop + 10 >= currentTarget.scrollHeight - currentTarget.clientHeight;
}
function SortIcon({ sortDirection }: SortIconProps) {
const asc = sortDirection === 'ASC'
const desc = sortDirection === 'DESC'
return <div className="yy-table-column-sorter yy-table-column-sorter-full">
<span className="yy-table-column-sorter-inner">
{upNode(asc)}
{downNode(desc)}
</span>
</div>
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment