Commit bdd897ee by zhaochengxiang

Merge branch 'virtual' into 'master'

Virtual

See merge request !9
parents 9759856d 247c09f0
const CracoLessPlugin = require('craco-less'); const CracoLessPlugin = require('craco-less');
const { loaderByName } = require("@craco/craco");
const { name } = require('./package'); const { name } = require('./package');
module.exports = { module.exports = {
...@@ -6,6 +7,24 @@ module.exports = { ...@@ -6,6 +7,24 @@ module.exports = {
{ {
plugin: CracoLessPlugin, plugin: CracoLessPlugin,
options: { options: {
modifyLessRule(lessRule, context) {
// You have to exclude these file suffixes first,
// if you want to modify the less module's suffix
lessRule.exclude = /\.m\.less$/;
return lessRule;
},
modifyLessModuleRule(lessModuleRule, context) {
// Configure the file suffix
lessModuleRule.test = /\.m\.less$/;
// Configure the generated local ident name.
const cssLoader = lessModuleRule.use.find(loaderByName("css-loader"));
cssLoader.options.modules = {
localIdentName: "[local]_[hash:base64:5]",
};
return lessModuleRule;
},
lessLoaderOptions: { lessLoaderOptions: {
lessOptions: { lessOptions: {
modifyVars: { modifyVars: {
...@@ -30,8 +49,18 @@ module.exports = { ...@@ -30,8 +49,18 @@ module.exports = {
libraryTarget: 'umd', libraryTarget: 'umd',
chunkLoadingGlobal: `webpackJsonp_${name}`, chunkLoadingGlobal: `webpackJsonp_${name}`,
globalObject: 'window', globalObject: 'window',
} },
} module: {
rules: [
{
test: /\.m?js/,
resolve: {
fullySpecified: false,
},
},
],
},
},
}, },
devServer: { devServer: {
headers: { headers: {
......
...@@ -11,10 +11,10 @@ ...@@ -11,10 +11,10 @@
"@testing-library/jest-dom": "^5.11.4", "@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0", "@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10", "@testing-library/user-event": "^12.1.10",
"@types/jest": "^27.4.1", "@types/jest": "^27.5.1",
"@types/node": "^17.0.21", "@types/node": "16.11.12",
"@types/react": "^17.0.39", "@types/react": "17.0.33",
"@types/react-dom": "^17.0.12", "@types/react-dom": "17.0.10",
"ahooks": "^3.1.7", "ahooks": "^3.1.7",
"antd": "4.18.2", "antd": "4.18.2",
"axios": "^0.19.0", "axios": "^0.19.0",
...@@ -31,6 +31,8 @@ ...@@ -31,6 +31,8 @@
"local-storage": "^2.0.0", "local-storage": "^2.0.0",
"react": "^17.0.1", "react": "^17.0.1",
"react-contexify": "^5.0.0", "react-contexify": "^5.0.0",
"react-contextmenu": "2.14.0",
"react-data-grid": "7.0.0-beta.13",
"react-dnd": "^14.0.2", "react-dnd": "^14.0.2",
"react-dnd-html5-backend": "^14.0.0", "react-dnd-html5-backend": "^14.0.0",
"react-dom": "^17.0.1", "react-dom": "^17.0.1",
...@@ -44,7 +46,7 @@ ...@@ -44,7 +46,7 @@
"redux-saga": "^1.0.5", "redux-saga": "^1.0.5",
"showdown": "^1.9.1", "showdown": "^1.9.1",
"smooth-scroll": "^16.1.3", "smooth-scroll": "^16.1.3",
"typescript": "^4.6.2", "typescript": "4.4.4",
"web-vitals": "^1.0.1" "web-vitals": "^1.0.1"
}, },
"scripts": { "scripts": {
......
declare const rctable: any;
declare module 'rc-table' {
export default rctable;
}
declare const nprogress: any;
declare module 'nprogress' {
export default nprogress;
}
declare module 'react-resizable' {
export const Resizable:any
}
declare module 'd3v3'
declare module '*' // for import jsx
\ No newline at end of file
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
@import '~antd/dist/antd.less'; @import '~antd/dist/antd.less';
@import './mixins.less'; @import './mixins.less';
@import './variables.less'; @import './variables.less';
@import './view/Manage/VirtualTable/mixins.less';
//与center-home中的样式保持统一 //与center-home中的样式保持统一
body { body {
...@@ -214,6 +215,10 @@ tr.drop-over-upward td { ...@@ -214,6 +215,10 @@ tr.drop-over-upward td {
color: #5B5B5B; color: #5B5B5B;
} }
.anchor {
background-color: #e7f7ff;
}
.m-common { .m-common {
margin: 20px 15px; margin: 20px 15px;
} }
......
/// <reference types="react-scripts" /> declare const rctable: any;
declare module 'rc-table' {
export default rctable;
}
declare const nprogress: any;
declare module 'nprogress' {
export default nprogress;
}
declare module 'react-resizable' {
export const Resizable:any
}
declare module 'd3v3'
declare module "*.module.less" {
const classes: { readonly [key: string]: string };
export default classes;
}
declare module '*' // for import jsx
\ No newline at end of file
...@@ -182,6 +182,7 @@ const EditModel = (props) => { ...@@ -182,6 +182,7 @@ const EditModel = (props) => {
setActionData({ ...actionData, ...{ action: 'detail', modelerId: data.id||'', editable: data?.editable||false, stateId: data?.state?.id||'' } }); setActionData({ ...actionData, ...{ action: 'detail', modelerId: data.id||'', editable: data?.editable||false, stateId: data?.state?.id||'' } });
actionRef.current = 'detail'; actionRef.current = 'detail';
LocalStorage.set('modelId', data.id||'');
LocalStorage.set('modelChange', !(LocalStorage.get('modelChange')||false)); LocalStorage.set('modelChange', !(LocalStorage.get('modelChange')||false));
}, },
error: () => { error: () => {
...@@ -199,6 +200,7 @@ const EditModel = (props) => { ...@@ -199,6 +200,7 @@ const EditModel = (props) => {
setActionData({ ...actionData, ...{ action: (_action==='flow')?'flow':'detail', modelerId: data.id||'', stateId: data?.state?.id||'', permitCheckOut: data?.permitCheckOut||false, editable: data?.editable||false } }); setActionData({ ...actionData, ...{ action: (_action==='flow')?'flow':'detail', modelerId: data.id||'', stateId: data?.state?.id||'', permitCheckOut: data?.permitCheckOut||false, editable: data?.editable||false } });
actionRef.current = (_action==='flow')?'flow':'detail'; actionRef.current = (_action==='flow')?'flow':'detail';
LocalStorage.set('modelId', data.id||'');
LocalStorage.set('modelChange', !(LocalStorage.get('modelChange')||false)); LocalStorage.set('modelChange', !(LocalStorage.get('modelChange')||false));
} }
}, },
......
import React, { useState, useEffect } from "react";
import { Tooltip, Modal, Table, Typography } from 'antd';
import classnames from 'classnames';
import { Resizable } from 'react-resizable';
import ResizeObserver from 'rc-resize-observer';
import { ContextMenu, ContextMenuTrigger } from 'react-contextmenu';
import { nanoid } from 'nanoid';
import { createPortal } from 'react-dom';
import { MenuItem } from 'react-contextmenu';
import { dispatch } from '../../../../model';
import { showMessage, isSzseEnv, formatDate, getDataModelerRole } from '../../../../util';
import { Action, CatalogId, ModelerId, DataModelerRoleReader } from '../../../../util/constant';
// import Tag from "../../Tag";
import './ModelTable.less';
const { Text } = Typography;
const { Column } = Table;
const ModelNameColumn = (props) => {
const { text, record, detailItem } = props;
const [ data, setData ] = useState(record);
const cols = [
{
title: '序号',
dataIndex: 'key',
render: (text, record, index) => {
return (index+1).toString();
},
width: 60,
ellipsis: true,
},
{
title: '字段中文名称',
width: 160,
dataIndex: 'cnName',
editable: true,
ellipsis: true,
},
{
title: '字段英文名称',
width: 160,
dataIndex: 'name',
editable: true,
ellipsis: true,
},
];
let _textComponent = <span style={{ color: '#000' }}>{text}</span>;
if (data.digest) {
_textComponent = <div style={{ width: 500, maxHeight: 300, overflow: 'auto' }}>
<Table
rowKey='name'
dataSource={data.digest.attributeDigests||[]}
columns={cols}
loading={false}
pagination={false}
size='small'
rowClassName={(record, index) => {
if (record?.primaryKey) {
return 'primary-row';
}
return '';
}}
/>
</div>;
}
return (
<Tooltip
title={_textComponent}
overlayClassName='tooltip-common'
onVisibleChange={(visible) => {
if (visible && !record.digest) {
dispatch({
type: 'datamodel.getDataModelDigest',
payload: {
id: record.id
},
callback: _data => {
record.digest = _data;
setData({...record});
}
})
}
}}
>
<a onClick={()=>{detailItem(record);}}>
{text||''}
</a>
</Tooltip>
);
}
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 ExpandedModelTable = (props) => {
const { onChange, onItemAction, onHistory, onAutoCreateTable, onExpandedSelect, onExpandedChange, catalogId, keyword, id = null, pid = null, view, user, checked, dataMap } = props;
const [ tableWidth, setTableWidth ] = useState(0);
const [ selectedRowKeys, setSelectedRowKeys ] = useState([]);
const [ data, setData ] = useState([]);
const [ columns, setColumns ] = useState([]);
const [ currentItem, setCurrentItem ] = useState(null);
const [modal, contextHolder] = Modal.useModal();
const cols = [
{
title: '模型名称',
dataIndex: 'name',
width: isSzseEnv?360:160,
ellipsis: true,
render: (text, record, index) => {
return (<ModelNameColumn text={text} record={record} detailItem={detailItem} />);
}
},
{
title: '中文名称',
dataIndex: 'cnName',
width: isSzseEnv?420:160,
ellipsis: true,
render: (text, _, __) => {
return (
<Tooltip title={text||''}>
<Text ellipsis={true}>{text||''}</Text>
</Tooltip>
)
}
},
{
title: '状态',
dataIndex: 'state',
width: 100,
ellipsis: true,
render: (_, record) => {
let color = '';
if (record?.state?.id === '1') {
color = '#DE7777';
} else if (record?.state?.id === '2') {
color = '#779BDE';
} else if (record?.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>
);
}
},
{
title: '创建人',
dataIndex: 'editor',
width: 100,
ellipsis: true,
},
{
title: '版本号',
dataIndex: 'modifiedTs',
width: 170,
ellipsis: true,
render: (_,record) => {
return `V_${formatDate(record.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,
render: (text, _, __) => {
return (
<Tooltip title={text||''} overlayClassName='tooltip-common'>
<Text ellipsis={true}>{text||''}</Text>
</Tooltip>
);
}
},
];
const pathColumn = {
title: '路径',
dataIndex: 'path',
width: 120,
ellipsis: true,
render: (text, _, __) => {
return (
<Tooltip title={text||''}>
<Text ellipsis={true}>{text||''}</Text>
</Tooltip>
)
}
};
useEffect(() => {
setSelectedRowKeys(checked?[id]:[]);
if (dataMap.has(id)) {
setData([dataMap.get(id)]);
} else {
getCheckoutDataModel();
}
//eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
useEffect(() => {
if (tableWidth>0 && columns.length===0) {
let newColumns = [...cols];
if (view==='state' || (keyword||'')!=='') {
newColumns.splice(3, 0, pathColumn);
}
newColumns.forEach((column, index) => {
if (!column.width) {
const rowWidth = (newColumns.reduce((preVal, col) => (col.width?col.width:0) + preVal, 0)) + 97;
if (tableWidth - rowWidth > 200) {
column.width = tableWidth-rowWidth;
} else {
newColumns.width = 200;
}
}
});
setColumns([ ...newColumns, <Column key='auto' />]);
}
//eslint-disable-next-line react-hooks/exhaustive-deps
}, [ tableWidth ])
const getCheckoutDataModel = () => {
dispatch({
type: 'datamodel.getCheckoutDataModel',
payload: {
id: pid
},
callback: data => {
setData(data?[data]:[]);
onExpandedChange && onExpandedChange(id, data);
}
})
}
const editItem = (record) => {
onItemAction && onItemAction(record, 'edit');
}
const detailItem = (record) => {
onItemAction && onItemAction(record, 'detail', getDataModelerRole(user)===DataModelerRoleReader);
}
const deployAction = (record) => {
onAutoCreateTable && onAutoCreateTable(record);
}
const stateAction = (record, action) => {
modal.confirm({
title: '提示!',
content: `您确定要${action.cnName||''}该模型吗?`,
onOk: () => {
dispatch({
type: 'datamodel.nextState',
payload: {
easyDataModelerDataModelId: record.id,
actionId: action.id
},
callback: () => {
showMessage('success', `模型${action.cnName||''}成功`);
if (action.id === '2') {
onChange && onChange();
} else {
getCheckoutDataModel();
}
}
})
}
});
}
const deleteItem = (record) => {
modal.confirm({
title: '提示!',
content: '您确定要删除该模型吗?',
onOk: () => {
dispatch({
type: 'datamodel.deleteDataModel',
payload: {
params: {
id: record.id
}
},
callback: () => {
showMessage('success', '模型删除成功');
onChange && onChange();
}
})
}
});
}
const historyItem = (record) => {
onHistory && onHistory(record.id);
}
const onExpandedSelectChange = keys => {
setSelectedRowKeys(keys);
onExpandedSelect && onExpandedSelect(keys, data[0].id);
};
const handleResize = index => (e, { size }) => {
let nextColumns = [...columns];
nextColumns[index] = {
...nextColumns[index],
width: size.width,
};
setColumns(nextColumns);
};
const rowSelection = {
selectedRowKeys,
onChange: onExpandedSelectChange,
hideSelectAll: true,
};
const classes = classnames('model-table', {
'model-table-sub': id
});
const handleItemClick = (e, data) => {
const { key } = data;
if (key === 'edit') {
editItem(currentItem);
} else if (key === 'delete') {
deleteItem(currentItem);
} else if (key === 'history') {
historyItem(currentItem);
} else if (key === 'copy') {
window.open(`/data-govern/data-model-action?${Action}=add&${CatalogId}=${(view==='dir')?(catalogId||''):''}&${ModelerId}=${currentItem.id}`);
} else if (key === 'createTable') {
deployAction(currentItem);
} else if (key.indexOf('action') !== -1) {
const index = (key.split('-'))[1];
const action = currentItem?.state?.supportedActions[index];
stateAction(currentItem, action);
}
}
const mergedColumns = () => {
let newColumns = [...columns];
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') {
disableEdit = true;
if (currentItem?.state?.id === '2') {
editTip = '待发布的模型不允许编辑';
}
}
if (!currentItem?.permitCheckOut && currentItem?.state?.id==='4') {
disableEdit = true;
editTip = `${currentItem.holder||''}正在编辑中, 不允许再编辑`;
editMenuTitle = `编辑(${currentItem.holder||''}正在编辑中)`;
}
if (!currentItem?.deletable) {
disableDelete = true;
if (currentItem?.state?.id === '2') {
deleteTip = '待发布的模型不允许删除';
} else if (currentItem?.state?.id === '4') {
deleteTip = '已发布的模型不允许删除';
}
}
const contextMenuId = nanoid()
return (
<div className={classes}>
<ResizeObserver
onResize={({ width }) => {
setTableWidth(width);
}}
>
<Table
rowSelection={rowSelection}
components={{
header: {
cell: ResizeableHeaderCell,
},
body: {
row: (props) => {
return (
<ContextMenuTrigger id={contextMenuId} >
<tr {...props} />
</ContextMenuTrigger>
)
},
},
}}
columns={mergedColumns()}
rowKey={'id'}
dataSource={data||[]}
pagination={false}
size='small'
onRow={(record, index) => {
return {
onContextMenu: event => {
setCurrentItem(record);
},
}
}}
/>
</ResizeObserver>
{createPortal(
<ContextMenu id={contextMenuId} rtl={false}>
{
(getDataModelerRole(user)!==DataModelerRoleReader) && <MenuItem data={{ key: 'edit' }} disabled={disableEdit} onClick={handleItemClick}>
<Tooltip title={editTip}>
{ editMenuTitle }
</Tooltip>
</MenuItem>
}
{
(getDataModelerRole(user)!==DataModelerRoleReader) && <MenuItem data={{ key: 'delete' }} disabled={disableDelete} onClick={handleItemClick}>
<Tooltip title={deleteTip}>
删除
</Tooltip>
</MenuItem>
}
<MenuItem data={{ key: 'history' }} onClick={handleItemClick}>
历史版本
</MenuItem>
{
(getDataModelerRole(user)!==DataModelerRoleReader) && <MenuItem data={{ key: 'copy' }} onClick={handleItemClick}>
复制模型
</MenuItem>
}
{
getDataModelerRole(user)!==DataModelerRoleReader && (currentItem?.state?.supportedActions||[]).length>0 && currentItem?.state?.supportedActions.map((item, index) => {
return (
<MenuItem data={{ key: `action-${index}` }} onClick={handleItemClick}>
{item.cnName||''}
</MenuItem>
);
})
}
{
getDataModelerRole(user)!==DataModelerRoleReader &&currentItem?.deployable && <MenuItem id='createTable' onClick={handleItemClick}>
建表
</MenuItem>
}
</ContextMenu>
,
document.body
)}
{ contextHolder }
</div>
);
}
export default ExpandedModelTable;
\ No newline at end of file
import React, { useState, useEffect, useRef } from "react"; import React, { useState, useEffect, useRef, useMemo } from "react";
import { Tooltip, Modal, Pagination, Table, Typography } from 'antd'; import { Tooltip, Modal, Table, Typography } from 'antd';
import { DownOutlined, UpOutlined } from '@ant-design/icons';
import SmoothScroll from 'smooth-scroll'; import SmoothScroll from 'smooth-scroll';
import classnames from 'classnames'; import { MenuItem } from 'react-contextmenu';
import { Resizable } from 'react-resizable'; import LocalStorage from 'local-storage';
import { useContextMenu, Menu as RcMenu, Item as RcItem } from "react-contexify";
import ResizeObserver from 'rc-resize-observer';
import DataGrid from '../../VirtualTable';
import { dispatch } from '../../../../model'; import { dispatch } from '../../../../model';
import { showMessage, getQueryParam, paginate, isSzseEnv, formatDate, getDataModelerRole } from '../../../../util'; import { showMessage, getQueryParam, isSzseEnv, formatDate, getDataModelerRole } from '../../../../util';
import { AnchorId, AnchorTimestamp, Action, CatalogId, ModelerId, DataModelerRoleReader } from '../../../../util/constant'; import { AnchorId, AnchorTimestamp, Action, CatalogId, ModelerId, DataModelerRoleReader } from '../../../../util/constant';
import ExpandedModelTable from "./ExpandedModelTable";
// import Tag from "../../Tag"; // import Tag from "../../Tag";
import './ModelTable.less'; import './ModelTable.less';
import 'react-contexify/dist/ReactContexify.css';
const { Text } = Typography; const { Text } = Typography;
const { Column } = Table;
const ModelNameColumn = (props) => { const ModelNameColumn = (props) => {
const { text, record, detailItem } = props; const { text, record, detailItem } = props;
...@@ -52,6 +49,7 @@ const ModelNameColumn = (props) => { ...@@ -52,6 +49,7 @@ const ModelNameColumn = (props) => {
if (data.digest) { if (data.digest) {
_textComponent = <div style={{ width: 500, maxHeight: 300, overflow: 'auto' }}> _textComponent = <div style={{ width: 500, maxHeight: 300, overflow: 'auto' }}>
<Table <Table
rowKey='name'
dataSource={data.digest.attributeDigests||[]} dataSource={data.digest.attributeDigests||[]}
columns={cols} columns={cols}
loading={false} loading={false}
...@@ -94,245 +92,150 @@ const ModelNameColumn = (props) => { ...@@ -94,245 +92,150 @@ 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 ModelTable = (props) => {
const { data, onChange, onItemAction, onSelect, onHistory, catalogId, keyword, onAutoCreateTable, offset = null, view, modelState, user } = props;
const { data, onChange, onItemAction, onSelect, onHistory, catalogId, keyword, onAutoCreateTable, offset = null, modelId = null, modelPid = null, view, selectModelerIds, onSubSelect, modelState, user } = props; const [ selectedRowKeys, setSelectedRowKeys ] = useState([]);
const [ expandedSelectedRowKeys, setExpandedSelectedRowKeys ] = useState([]);
const MENU_ID = (((modelId||'') !== '') ? `model-table-contextmenu-${modelId}` : 'model-table-contextmenu'); const expandedDataMapRef = useRef(new Map());
const shouldScrollRef = useRef(false);
const { show } = useContextMenu({ const anchorId = getQueryParam(AnchorId, props?.location?.search);
id: MENU_ID, const anchorTimestamp = getQueryParam(AnchorTimestamp, props?.location?.search);
});
const [ tableWidth, setTableWidth ] = useState(0); const [modal, contextHolder] = Modal.useModal();
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 = [ const cols = [
{ {
title: '序号', name: '序号',
dataIndex: 'key', key: 'index',
render: (text, record, index) => {
return (index+1).toString();
},
width: 60, width: 60,
ellipsis: true, sortable: false,
}, },
{ {
title: '模型名称', name: '模型名称',
dataIndex: 'name', key: 'name',
width: isSzseEnv?360:160, width: isSzseEnv?360:160,
ellipsis: true, sortable: true,
sorter: true, resizable: true,
sortDirections: ['ascend', 'descend'], formatter(props) {
render: (text, record, index) => { return (<ModelNameColumn text={props.row.name} record={props.row} detailItem={detailItem} />);
return (<ModelNameColumn text={text} record={record} detailItem={detailItem} />);
} }
}, },
{ {
title: '中文名称', name: '中文名称',
dataIndex: 'cnName', key: 'cnName',
width: isSzseEnv?420:160, width: isSzseEnv?420:160,
ellipsis: true, sortable: true,
sorter: true, resizable: true,
sortDirections: ['ascend', 'descend'], formatter(props) {
render: (text, _, __) => {
return ( return (
<Tooltip title={text||''}> <Tooltip title={props.row.cnName||''}>
<Text ellipsis={true}>{text||''}</Text> <Text ellipsis={true}>{props.row.cnName||''}</Text>
</Tooltip> </Tooltip>
) )
} }
}, },
{ {
title: '状态', name: '状态',
dataIndex: 'state', key: 'state',
width: 100, width: 100,
ellipsis: true, sortable: true,
sorter: true, resizable: true,
sortDirections: ['ascend', 'descend'], formatter(props) {
render: (_, record) => {
let color = ''; let color = '';
if (record?.state?.id === '1') { if (props.row.state?.id === '1') {
color = '#DE7777'; color = '#DE7777';
} else if (record?.state?.id === '2') { } else if (props.row.state?.id === '2') {
color = '#779BDE'; color = '#779BDE';
} else if (record?.state?.id === '4') { } else if (props.row.state?.id === '4') {
color = '#77DEBF'; color = '#77DEBF';
} }
return ( return (
<span> <span>
<span style={{ display: 'inline-block', width: 10, height: 10, borderRadius: 5, marginRight: 5, backgroundColor: color }}></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> </span>
); );
} }
}, },
{ {
title: '创建人', name: '创建人',
dataIndex: 'editor', key: 'editor',
width: 100, width: 100,
ellipsis: true, sortable: true,
sorter: true, resizable: true,
sortDirections: ['ascend', 'descend'],
}, },
{ {
title: '版本号', name: '版本号',
dataIndex: 'modifiedTs', key: 'modifiedTs',
width: 170, width: 170,
ellipsis: true, sortable: true,
sorter: true, resizable: true,
sortDirections: ['ascend', 'descend'], formatter(props) {
render: (_,record) => { return `V_${formatDate(props.row.modifiedTs)}`;
return `V_${formatDate(record.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: '模型描述', name: '模型描述',
dataIndex: 'remark', key: 'remark',
ellipsis: true, sortable: true,
sorter: true, resizable: true,
sortDirections: ['ascend', 'descend'], formatter(props) {
render: (text, _, __) => {
return ( return (
<Tooltip title={text||''} overlayClassName='tooltip-common'> <Tooltip title={props.row.remark||''} overlayClassName='tooltip-common'>
<Text ellipsis={true}>{text||''}</Text> <Text ellipsis={true}>{props.row.remark||''}</Text>
</Tooltip> </Tooltip>
); )
} }
}, },
]; ];
const pathColumn = { const pathColumn = {
title: '路径', name: '路径',
dataIndex: 'path', key: 'path',
width: 120, width: 120,
ellipsis: true, sortable: true,
sorter: true, resizable: true,
sortDirections: ['ascend', 'descend'], formatter(props) {
render: (text, _, __) => {
return ( return (
<Tooltip title={text||''}> <Tooltip title={props.row.path||''}>
<Text ellipsis={true}>{text||''}</Text> <Text ellipsis={true}>{props.row.path||''}</Text>
</Tooltip> </Tooltip>
) )
} }
}; };
const [ columns, setColumns ] = useState([]); const columns = useMemo(() => {
const [ includePathColumns, setIncludePathColumns ] = useState([]); const newColumns = [...cols];
const [ pagination, setPagination ] = useState( { pageNum: 1, pageSize: 20 } ); if (view==='state' || (keyword||'')!=='') {
const [ currentItem, setCurrentItem ] = useState(null); newColumns.splice(3, 0, pathColumn);
const { pageNum, pageSize } = pagination; }
return newColumns;
const [modal, contextHolder] = Modal.useModal(); // eslint-disable-next-line react-hooks/exhaustive-deps
}, [view, keyword]);
const anchorId = getQueryParam(AnchorId, props.location.search);
const anchorTimestamp = getQueryParam(AnchorTimestamp, props.location.search);
const shouldScrollRef = useRef(false);
useEffect(() => { useEffect(() => {
if ((modelId||'') !== '') {
window?.addEventListener("storage", modelEventChange); window?.addEventListener("storage", modelEventChange);
return () => { return () => {
window?.removeEventListener("storage", modelEventChange); window?.removeEventListener("storage", modelEventChange);
} }
}
//eslint-disable-next-line react-hooks/exhaustive-deps //eslint-disable-next-line react-hooks/exhaustive-deps
}, []) }, [])
useEffect(() => { useEffect(() => {
setSelectedRowKeys([]);
if ((modelId||'') === '') { setExpandedSelectedRowKeys([]);
onSelect && onSelect([]); onSelect && onSelect([]);
if ((keyword||'') === '') {
if (offset !== null) {
const _pageNum = parseInt(offset/pageSize + ((offset%pageSize===0)?0:1));
setPagination({...pagination, pageNum: _pageNum });
} else {
setPagination({...pagination, pageNum: 1 });
}
} else {
setPagination({...pagination, pageNum: 1 });
}
} else {
getCheckoutDataModel();
}
//eslint-disable-next-line react-hooks/exhaustive-deps //eslint-disable-next-line react-hooks/exhaustive-deps
}, [ catalogId, keyword, offset, modelState ]); }, [ catalogId, keyword, offset, modelState ]);
useEffect(() => { useEffect(() => {
if ((selectModelerIds||[]).length === 0) {
setSelectedRowKeys([]);
setSubSelectedRowKeys([]);
}
}, [selectModelerIds])
useEffect(() => {
if ((anchorId||'') !== '') { if ((anchorId||'') !== '') {
shouldScrollRef.current = true; shouldScrollRef.current = true;
} }
//eslint-disable-next-line react-hooks/exhaustive-deps //eslint-disable-next-line react-hooks/exhaustive-deps
}, [anchorTimestamp]) }, [anchorTimestamp])
...@@ -342,7 +245,7 @@ const ModelTable = (props) => { ...@@ -342,7 +245,7 @@ const ModelTable = (props) => {
const _id = getQueryParam(AnchorId, props.location.search); const _id = getQueryParam(AnchorId, props.location.search);
var scroll = new SmoothScroll(); var scroll = new SmoothScroll();
var anchor = document.querySelector(`#data-model-${_id}`); var anchor = document.querySelector(`#row-${_id}`);
if (anchor) { if (anchor) {
scroll.animateScroll(anchor); scroll.animateScroll(anchor);
...@@ -351,125 +254,12 @@ const ModelTable = (props) => { ...@@ -351,125 +254,12 @@ 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) => { const modelEventChange = (e) => {
if (e.key === 'modelChange') { if (e.key === 'modelChange') {
getCheckoutDataModel(); expandedDataMapRef.current.delete(LocalStorage.get('modelId'));
} }
} }
const getCheckoutDataModel = () => {
dispatch({
type: 'datamodel.getCheckoutDataModel',
payload: {
id: modelPid
},
callback: data => {
setSubData(data?[data]:[]);
},
error: () => {
}
})
}
// const getDataModel = () => {
// dispatch({
// type: 'datamodel.getDataModel',
// payload: {
// id: modelId
// },
// callback: data => {
// setSubData(data?[data]:[]);
// },
// error: () => {
// }
// })
// }
const editItem = (record) => { const editItem = (record) => {
onItemAction && onItemAction(record, 'edit'); onItemAction && onItemAction(record, 'edit');
} }
...@@ -496,7 +286,6 @@ const ModelTable = (props) => { ...@@ -496,7 +286,6 @@ const ModelTable = (props) => {
callback: () => { callback: () => {
showMessage('success', `模型${action.cnName||''}成功`); showMessage('success', `模型${action.cnName||''}成功`);
if ((modelId||'') === '') {
onChange && onChange(); onChange && onChange();
const index = selectedRowKeys.findIndex((rowKey) => rowKey === record.id); const index = selectedRowKeys.findIndex((rowKey) => rowKey === record.id);
...@@ -506,14 +295,6 @@ const ModelTable = (props) => { ...@@ -506,14 +295,6 @@ const ModelTable = (props) => {
setSelectedRowKeys(newSelectedRowKeys); setSelectedRowKeys(newSelectedRowKeys);
onSelect && onSelect(newSelectedRowKeys); onSelect && onSelect(newSelectedRowKeys);
} }
} else {
if (action.id === '2') {
onChange && onChange();
} else {
getCheckoutDataModel();
}
}
} }
}) })
} }
...@@ -521,7 +302,6 @@ const ModelTable = (props) => { ...@@ -521,7 +302,6 @@ const ModelTable = (props) => {
} }
const deleteItem = (record) => { const deleteItem = (record) => {
modal.confirm({ modal.confirm({
title: '提示!', title: '提示!',
content: '您确定要删除该模型吗?', content: '您确定要删除该模型吗?',
...@@ -537,7 +317,6 @@ const ModelTable = (props) => { ...@@ -537,7 +317,6 @@ const ModelTable = (props) => {
showMessage('success', '模型删除成功'); showMessage('success', '模型删除成功');
onChange && onChange(); onChange && onChange();
if ((modelId||'') ==='') {
const index = selectedRowKeys.findIndex((rowKey) => rowKey === record.id); const index = selectedRowKeys.findIndex((rowKey) => rowKey === record.id);
if (index !== -1) { if (index !== -1) {
const newSelectedRowKeys = [...selectedRowKeys]; const newSelectedRowKeys = [...selectedRowKeys];
...@@ -546,8 +325,6 @@ const ModelTable = (props) => { ...@@ -546,8 +325,6 @@ const ModelTable = (props) => {
onSelect && onSelect(newSelectedRowKeys); onSelect && onSelect(newSelectedRowKeys);
} }
} }
}
}) })
} }
}); });
...@@ -559,66 +336,30 @@ const ModelTable = (props) => { ...@@ -559,66 +336,30 @@ const ModelTable = (props) => {
const onSelectChange = keys => { const onSelectChange = keys => {
setSelectedRowKeys(keys); setSelectedRowKeys(keys);
onSelect && onSelect([...expandedSelectedRowKeys, ...keys]);
if ((modelId||'') !== '') {
onSubSelect && onSubSelect(keys, subData[0].id);
} else {
onSelect && onSelect([...subSelectedRowKeys, ...keys]);
}
}; };
const onSubSelectChange = (keys, id) => { const onExpandedTableSelectChange = (keys, id) => {
if ((keys||[]).length === 0) { if ((keys||[]).length === 0) {
const index = subSelectedRowKeys.findIndex((rowKey) => rowKey === id); const index = expandedSelectedRowKeys.findIndex((rowKey) => rowKey === id);
const newSubSelectedRowKeys = [...subSelectedRowKeys]; const newExpandedSelectedRowKeys = [...expandedSelectedRowKeys];
newSubSelectedRowKeys.splice(index, 1); newExpandedSelectedRowKeys.splice(index, 1);
setSubSelectedRowKeys(newSubSelectedRowKeys); setExpandedSelectedRowKeys(newExpandedSelectedRowKeys);
onSelect && onSelect([...newSubSelectedRowKeys, ...selectedRowKeys]); onSelect && onSelect([...newExpandedSelectedRowKeys, ...selectedRowKeys]);
} else { } else {
const newSubSelectedRowKeys = [...subSelectedRowKeys, id]; const newExpandedSelectedRowKeys = [...expandedSelectedRowKeys, id];
onSelect && onSelect([...newSubSelectedRowKeys, ...selectedRowKeys]); setExpandedSelectedRowKeys(newExpandedSelectedRowKeys);
} onSelect && onSelect([...newExpandedSelectedRowKeys, ...selectedRowKeys]);
} }
const handleResize = index => (e, { size }) => {
let nextColumns = [...columns];
if ((modelId||'')==='' && (view==='state'||(keyword||'')!=='')) {
nextColumns = [...includePathColumns];
} }
nextColumns[index] = { const onExpandedDataMapChange = (id, value) => {
...nextColumns[index], expandedDataMapRef.current.set(id, value);
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
});
let expandable = undefined; let expandable = undefined;
if (!modelId) {
let needExpand = false; let needExpand = false;
(filterData||[]).forEach(record => { (data||[]).forEach(record => {
if (record?.alreadyCheckedOut) { if (record?.alreadyCheckedOut) {
needExpand = true; needExpand = true;
} }
...@@ -626,196 +367,160 @@ const ModelTable = (props) => { ...@@ -626,196 +367,160 @@ const ModelTable = (props) => {
if (needExpand) { if (needExpand) {
expandable = { expandable = {
expandedRowRender: record => <ModelTable expandedRowHeight: 100,
modelId={record?.checkedOutId} rowExpandable: (row) => {
modelPid={record?.id} return row?.alreadyCheckedOut;
onSubSelect={onSubSelectChange}
{...props}
/>,
expandIcon: ({ expanded, onExpand, record }) => {
if (!record?.alreadyCheckedOut) return null;
return expanded ? <UpOutlined style={{ fontSize: 10 }} onClick={e => onExpand(record, e)} /> : <DownOutlined style={{ fontSize: 10 }} onClick={e => onExpand(record, e)} />
},
rowExpandable: record => {
return record?.alreadyCheckedOut;
}
};
}
} else {
expandable = {
expandedRowRender: record => <></>,
expandIcon: ({ expanded, onExpand, record }) => {
return null;
}, },
rowExpandable: record => { expandRowRender: (row) => {
return false; return (
} <div style={{ padding: 10 }}>
<ExpandedModelTable
id={row?.checkedOutId}
pid={row?.id}
checked={expandedSelectedRowKeys.indexOf(row?.checkedOutId)!==-1}
dataMap={expandedDataMapRef.current}
onExpandedSelect={onExpandedTableSelectChange}
onExpandedChange={onExpandedDataMapChange}
{...props}
/>
</div>
)
} }
} }
const displayMenu = (e) => {
show(e);
} }
const handleItemClick = ({ event, props, data, triggerEvent }) => { const handleItemClick = (e, data) => {
const key = event.currentTarget.id; const { key, item } = data;
if (key === 'edit') { if (key === 'edit') {
editItem(currentItem); editItem(item);
} else if (key === 'delete') { } else if (key === 'delete') {
deleteItem(currentItem); deleteItem(item);
} else if (key === 'history') { } else if (key === 'history') {
historyItem(currentItem); historyItem(item);
} else if (key === 'copy') { } else if (key === 'copy') {
window.open(`/data-govern/data-model-action?${Action}=add&${CatalogId}=${(view==='dir')?(catalogId||''):''}&${ModelerId}=${currentItem.id}`); window.open(`/data-govern/data-model-action?${Action}=add&${CatalogId}=${(view==='dir')?(catalogId||''):''}&${ModelerId}=${item.id}`);
} else if (key === 'createTable') { } else if (key === 'createTable') {
deployAction(currentItem); deployAction(item);
} else if (key.indexOf('action') !== -1) { } else if (key.indexOf('action') !== -1) {
const index = (key.split('-'))[1]; const index = (key.split('-'))[1];
const action = currentItem?.state?.supportedActions[index]; const action = item?.state?.supportedActions[index];
stateAction(currentItem, action); stateAction(item, action);
} }
} }
const mergedColumns = () => { const onMenuRender = (row) => {
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 = '编辑'; let disableEdit = false, disableDelete = false, editTip = '', deleteTip = '', editMenuTitle = '编辑';
if (!currentItem?.editable && currentItem?.state?.id!=='4') { if (!row?.editable && row?.state?.id!=='4') {
disableEdit = true; disableEdit = true;
if (currentItem?.state?.id === '2') { if (row?.state?.id === '2') {
editTip = '待发布的模型不允许编辑'; editTip = '待发布的模型不允许编辑';
} }
} }
if (!currentItem?.permitCheckOut && currentItem?.state?.id==='4') { if (!row?.permitCheckOut && row?.state?.id==='4') {
disableEdit = true; disableEdit = true;
editTip = `${currentItem.holder||''}正在编辑中, 不允许再编辑`; editTip = `${row.holder||''}正在编辑中, 不允许再编辑`;
editMenuTitle = `编辑(${currentItem.holder||''}正在编辑中)`; editMenuTitle = `编辑(${row.holder||''}正在编辑中)`;
} }
if (!currentItem?.deletable) { if (!row?.deletable) {
disableDelete = true; disableDelete = true;
if (currentItem?.state?.id === '2') { if (row?.state?.id === '2') {
deleteTip = '待发布的模型不允许删除'; deleteTip = '待发布的模型不允许删除';
} else if (currentItem?.state?.id === '4') { } else if (row?.state?.id === '4') {
deleteTip = '已发布的模型不允许删除'; deleteTip = '已发布的模型不允许删除';
} }
} }
return ( return <React.Fragment>
<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}
/>
</ResizeObserver>
{ {
!modelId && (data||[]).length>0 && <Pagination (getDataModelerRole(user)!==DataModelerRoleReader) && <MenuItem key='edit' data={{ key: 'edit', item: row }} disabled={disableEdit} onClick={handleItemClick}>
className="text-center mt-3"
showSizeChanger
showQuickJumper
onChange={(_pageNum, _pageSize) => {
setPagination({ pageNum: _pageNum, pageSize: _pageSize || 20 });
}}
onShowSizeChange={(_pageNum, _pageSize) => {
setPagination({ pageNum: 1, pageSize: _pageSize });
}}
current={pageNum}
pageSize={pageSize}
defaultCurrent={1}
total={(data||[]).length}
pageSizeOptions={[10,20,50]}
showTotal={total => `共 ${total} 条`}
/>
}
<RcMenu id={MENU_ID}>
{
(getDataModelerRole(user)!==DataModelerRoleReader) && <RcItem id="edit" disabled={disableEdit} onClick={handleItemClick}>
<Tooltip title={editTip}> <Tooltip title={editTip}>
{ editMenuTitle } { editMenuTitle }
</Tooltip> </Tooltip>
</RcItem> </MenuItem>
} }
{ {
(getDataModelerRole(user)!==DataModelerRoleReader) && <RcItem id="delete" disabled={disableDelete} onClick={handleItemClick}> (getDataModelerRole(user)!==DataModelerRoleReader) && <MenuItem data={{ key: 'delete', item: row }} disabled={disableDelete} onClick={handleItemClick}>
<Tooltip title={deleteTip}> <Tooltip title={deleteTip}>
删除 删除
</Tooltip> </Tooltip>
</RcItem> </MenuItem>
} }
<RcItem id="history" onClick={handleItemClick}> <MenuItem data={{ key: 'history', item: row }} onClick={handleItemClick}>
历史版本 历史版本
</RcItem> </MenuItem>
{ {
(getDataModelerRole(user)!==DataModelerRoleReader) && <RcItem id="copy" onClick={handleItemClick}> (getDataModelerRole(user)!==DataModelerRoleReader) && <MenuItem data={{ key: 'copy', item: row }} onClick={handleItemClick}>
复制模型 复制模型
</RcItem> </MenuItem>
} }
{ {
getDataModelerRole(user)!==DataModelerRoleReader && (currentItem?.state?.supportedActions||[]).length>0 && currentItem?.state?.supportedActions.map((item, index) => { getDataModelerRole(user)!==DataModelerRoleReader && (row?.state?.supportedActions||[]).length>0 && row?.state?.supportedActions.map((item, index) => {
return ( return (
<RcItem id={`action-${index}`} onClick={handleItemClick}> <MenuItem key={index} data={{ key: `action-${index}`, item: row }} onClick={handleItemClick}>
{item.cnName||''} {item.cnName||''}
</RcItem> </MenuItem>
); );
}) })
} }
{ {
getDataModelerRole(user)!==DataModelerRoleReader &&currentItem?.deployable && <RcItem id='createTable' onClick={handleItemClick}> getDataModelerRole(user)!==DataModelerRoleReader &&row?.deployable && <MenuItem data={{ key: "createTable", item: row }} onClick={handleItemClick}>
建表 建表
</RcItem> </MenuItem>
}
</React.Fragment>
} }
</RcMenu> return (
<div>
<DataGrid
style={{ blockSize: 'calc(100vh - 94px - 37px - 57px - 24px)' }}
checkable
columns={columns}
rows={data||[]}
rowHeight={51}
rowClassName={(row) => {
return (row.id === anchorId)?'anchor':''
}}
expandable={expandable}
contextMenu={{
menu: onMenuRender
}}
selectedRows={selectedRowKeys}
onSelectedRowsChange={onSelectChange}
getComparator={getComparator}
/>
{ contextHolder } { contextHolder }
</div> </div>
); );
} }
export default ModelTable; 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}"`);
}
}
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
display: none; display: none;
} }
.yy-table-tbody > tr:not(.yy-table-measure-row)> td { .yy-table-tbody tr:not(.yy-table-measure-row) td {
padding: 8px 8px !important; padding: 8px 8px !important;
} }
} }
......
import React, { ReactElement, useCallback, useEffect, useMemo, useState, forwardRef } from 'react';
import { createPortal } from 'react-dom';
import { ContextMenu, ContextMenuTrigger } from 'react-contextmenu';
import DataGrid, { Column, DataGridProps, Row as GridRow, RowsChangeData, SortColumn, SortIconProps, SelectColumn } from 'react-data-grid';
import type { DataGridHandle, CheckboxFormatterProps } from 'react-data-grid';
import { Checkbox, Empty } from 'antd';
import type { CheckboxChangeEvent } from 'antd/es/checkbox';
import { DownOutlined, UpOutlined } from '@ant-design/icons';
import { nanoid } from 'nanoid';
import { downNode, upNode } from './virtual-table-helper';
export enum RowAction {
None, Expand, Select
}
export enum RowType {
None, Detail
}
export interface RowData {
id: string,
index?: string;
__pid__?: string
__row__?: any
__type__?: RowType
__expanded__?: boolean
__selected__?: boolean
__action__?: RowAction
}
interface Props<Row> {
gridRef?: React.RefObject<DataGridHandle>
contextMenu?: {
id: string
menu: (row: RowData | undefined) => ReactElement
}
checkable: Boolean
expandable?: {
expandedRowHeight?: number
rowExpandable?: (row: Row) => Boolean
expandRowRender?: (row: Row) => React.ReactElement
}
getComparator?: (sortColumn: string) => (a: Row, b: Row) => number
loadMoreRows?: (length: number) => Promise<Row[]> | undefined
selectedRows?: Array<any>
onSelectedRowsChange?: (selectedRows: Array<any>) => void
rowHeight?: number
rowClassName?: (row: RowData) => string
}
const CheckboxFormatter = forwardRef<HTMLInputElement, CheckboxFormatterProps>(
function CheckboxFormatter({ onChange, ...props }: CheckboxFormatterProps, ref) {
function handleChange(e: CheckboxChangeEvent) {
onChange(e.target.checked, (e.nativeEvent as MouseEvent).shiftKey);
}
return <Checkbox ref={ref} {...props} onChange={handleChange} />;
}
);
export function getExpandingCol<Row extends RowData, SR>({ colSpan, expandRender, expandable }: {
colSpan: () => number
expandRender?: (row: Row) => React.ReactElement
expandable?: (row: Row) => Boolean
}) {
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 (expandRender) {
return expandRender(row.__row__)
}
return (
<div>{row.__pid__}</div>
);
}
if (!expandable || !expandable(row)) return null;
// 展开收起
return (
<div style={{ cursor: 'pointer', color: isCellSelected ? '#777' : '#ccc' }}
onClick={() => {
onRowChange({
...row,
__expanded__: !row.__expanded__,
__action__: RowAction.Expand
});
}}
>
{row.__expanded__ ? <UpOutlined /> : <DownOutlined />}
</div>
);
}
}
return col
}
function FC<Row extends RowData, SR, K extends React.Key = React.Key>(props: DataGridProps<Row, SR, K> & Props<Row>) {
const {
gridRef,
expandable,
getComparator,
loadMoreRows,
onSelectedRowsChange,
contextMenu, columns, rows, checkable, selectedRows, rowHeight = 45, rowClassName, ...rest } = props
const rowKeyGetter = (row: Row): K => {
return row.id as K;
}
const [contextItem, setContextItem] = useState<RowData|undefined>(undefined)
// 初始化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: `${row.id}-detail`, __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(() => {
const newRows = [...rows];
if (sortColumns.length > 0) {
newRows
// .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;
})
}
(newRows||[]).forEach((item, index) => {
item.index = `${index+1}`;
})
_setRows(newRows);
}, [rows, sortColumns, getComparator]);
// 组装功能s列
const cols = useMemo(() => {
if (expandable && checkable) {
const cols: readonly Column<Row, SR>[] = [
getExpandingCol<Row, SR>({
colSpan: () => columns.length+2, expandRender: expandable.expandRowRender, expandable: expandable.rowExpandable
}),
{ ...SelectColumn, key: 'select', frozen: false },
...columns,
]
return cols
} else if (expandable) {
const cols: readonly Column<Row, SR>[] = [
getExpandingCol<Row, SR>({
colSpan: () => columns.length+1, expandRender: expandable.expandRowRender, expandable: expandable.rowExpandable
}),
...columns,
]
return cols
} else if (checkable) {
const cols: readonly Column<Row, SR>[] = [
{ ...SelectColumn, key: 'select', frozen: false },
...columns,
]
return cols
}
return columns
//eslint-disable-next-line react-hooks/exhaustive-deps
}, [columns, expandable, checkable])
// 处理滚动
const handleScroll = useCallback((event: React.UIEvent<HTMLDivElement>) => {
if (!isAtBottom(event)) return;
loadMoreRows?.(rows.length)
}, [loadMoreRows, rows])
const contextMenuId = contextMenu?.id ?? nanoid()
return (
<React.Fragment>
<DataGrid
ref={gridRef}
{...rest}
columns={cols}
rows={_rows}
rowKeyGetter={rowKeyGetter}
components={{
sortIcon: SortIcon,
checkboxFormatter: CheckboxFormatter,
rowRenderer: (props) => {
return (
contextMenu ? <ContextMenuTrigger
id={contextMenuId}
collect={() => ({ rowIdx: props?.rowIdx })}
disable={props.row.__type__===RowType.Detail}
>
<GridRow
className={rowClassName(props.row)}
id={`row-${props.row.id}`}
onContextMenu={(e: React.MouseEvent) => {
setContextItem(props.row);
}}
{...props}
/>
</ContextMenuTrigger> : <React.Fragment></React.Fragment>
)
},
noRowsFallback: <div style={{ textAlign: 'center', gridColumn: '1/-1' }}>
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
</div>,
}}
onRowsChange={onRowsChange}
// headerRowHeight={45}
rowHeight={(args) => (args.type === 'ROW' && args.row.__type__ === RowType.Detail ? (expandable.expandedRowHeight||100) : rowHeight)}
// defaultColumnOptions={{
// sortable: true,
// resizable: true
// }}
// 排序
sortColumns={sortColumns}
onSortColumnsChange={setSortColumns}
selectedRows={new Set(selectedRows||[])}
onSelectedRowsChange={(values: Set<any>) => {
onSelectedRowsChange && onSelectedRowsChange(Array.from(values));
}}
// 滚动
onScroll={loadMoreRows ? handleScroll : undefined}
/>
{contextMenu && createPortal(
<ContextMenu id={contextMenuId} rtl={false}>
{contextMenu.menu(contextItem)}
</ContextMenu>
,
document.body
)}
</React.Fragment>
);
}
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
.rdg {
border: none !important;
}
.rdg-header-row {
.rdg-cell {
box-shadow: none !important;
background-color: #f2f5fc!important;
border-block-end: none !important;
border-inline-end: none !important;
font-weight: normal !important;
outline: none !important;
&:before {
position: absolute;
top: 50%;
right: 0;
width: 1px;
height: 1.6em;
background-color: rgba(0,0,0,.06);
-webkit-transform: translateY(-50%);
transform: translateY(-50%);
transition: background-color .3s;
content: "";
}
}
}
.rdg-row {
&:hover {
background-color: #fafafa;
}
.rdg-cell {
box-shadow: none !important;
color: #363636 !important;
border-block-end: 1px solid #f0f0f0 !important;
border-inline-end: none !important;
outline: none !important;
}
}
.react-contextmenu-wrapper {
display: contents;
}
.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;
}
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,
})}
/>
);
{ {
"compilerOptions": { "compilerOptions": {
"target": "es2016", "target": "es5",
"module": "esnext",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"lib": [ "lib": [
"dom", "dom",
"dom.iterable", "dom.iterable",
"esnext" "esnext"
], ],
"allowJs": true, "allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node", "moduleResolution": "node",
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
......
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