Allow reordering table columns using drag and drop in ERD Tool and create table. #4994

This commit is contained in:
Aditya Toshniwal 2022-10-03 11:18:22 +05:30 committed by GitHub
parent 869b90121c
commit 406119d96c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 250 additions and 49 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 116 KiB

View File

@ -15,6 +15,7 @@ Supported Database Servers
New features
************
| `Issue #4994 <https://github.com/pgadmin-org/pgadmin4/issues/4994>`_ - Allow reordering table columns using drag and drop in ERD Tool.
| `Issue #5304 <https://github.com/pgadmin-org/pgadmin4/issues/5304>`_ - Added high availability options to AWS deployment.
Housekeeping

View File

@ -146,6 +146,8 @@
"react-aspen": "^1.1.0",
"react-checkbox-tree": "^1.7.2",
"react-data-grid": "git+https://github.com/EnterpriseDB/react-data-grid.git/#200d2f5e02de694e3e9ffbe177c279bc40240fb8",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^17.0.1",
"react-draggable": "^4.4.4",
"react-dropzone": "^14.2.1",

View File

@ -661,6 +661,7 @@ export default class TableSchema extends BaseUISchema {
},
canAdd: this.canAddRowColumns,
canEdit: true, canDelete: true,
canReorder: (state)=>(this.inErd || this.isNew(state)),
// For each row edit/delete button enable/disable
canEditRow: this.canEditDeleteRowColumns,
canDeleteRow: this.canEditDeleteRowColumns,

View File

@ -15,12 +15,15 @@ import { makeStyles } from '@material-ui/core/styles';
import { PgIconButton } from '../components/Buttons';
import AddIcon from '@material-ui/icons/AddOutlined';
import { MappedCellControl } from './MappedControl';
import DragIndicatorRoundedIcon from '@material-ui/icons/DragIndicatorRounded';
import EditRoundedIcon from '@material-ui/icons/EditRounded';
import DeleteRoundedIcon from '@material-ui/icons/DeleteRounded';
import { useTable, useFlexLayout, useResizeColumns, useSortBy, useExpanded, useGlobalFilter } from 'react-table';
import clsx from 'clsx';
import PropTypes from 'prop-types';
import _ from 'lodash';
import { DndProvider, useDrag, useDrop } from 'react-dnd';
import {HTML5Backend} from 'react-dnd-html5-backend';
import gettext from 'sources/gettext';
import { SCHEMA_STATE_ACTIONS, StateUtilsContext } from '.';
@ -74,6 +77,15 @@ const useStyles = makeStyles((theme)=>({
overflow: 'auto',
backgroundColor: theme.otherVars.tableBg,
},
tableRowHovered: {
position: 'relative',
'& .hover-overlay': {
backgroundColor: theme.palette.primary.light,
position: 'absolute',
inset: 0,
opacity: 0.75,
}
},
tableCell: {
margin: 0,
padding: theme.spacing(0.5),
@ -96,6 +108,9 @@ const useStyles = makeStyles((theme)=>({
padding: theme.spacing(0.5, 0),
textAlign: 'center',
},
btnReorder: {
cursor: 'move',
},
resizer: {
display: 'inline-block',
width: '5px',
@ -152,10 +167,12 @@ DataTableHeader.propTypes = {
headerGroups: PropTypes.array.isRequired,
};
function DataTableRow({row, totalRows, isResizing, schema, schemaRef, accessPath}) {
function DataTableRow({index, row, totalRows, isResizing, isHovered, schema, schemaRef, accessPath, moveRow, setHoverIndex}) {
const classes = useStyles();
const [key, setKey] = useState(false);
const depListener = useContext(DepListenerContext);
const rowRef = useRef(null);
const dragHandleRef = useRef(null);
/* Memoize the row to avoid unnecessary re-render.
* If table data changes, then react-table re-renders the complete tables
@ -201,28 +218,87 @@ function DataTableRow({row, totalRows, isResizing, schema, schemaRef, accessPath
};
}, []);
const [{ handlerId }, drop] = useDrop({
accept: 'row',
collect(monitor) {
return {
handlerId: monitor.getHandlerId(),
};
},
hover(item, monitor) {
if (!rowRef.current) {
return;
}
item.hoverIndex = null;
// Don't replace items with themselves
if (item.index === index) {
return;
}
// Determine rectangle on screen
const hoverBoundingRect = rowRef.current?.getBoundingClientRect();
// Determine mouse position
const clientOffset = monitor.getClientOffset();
// Get pixels to the top
const hoverClientY = clientOffset.y - hoverBoundingRect.top;
// Only perform the move when the mouse has crossed certain part of the items height
// Dragging downwards
if (item.index < index && hoverClientY < (hoverBoundingRect.bottom - hoverBoundingRect.top)/3) {
return;
}
// Dragging upwards
if (item.index > index && hoverClientY > ((hoverBoundingRect.bottom - hoverBoundingRect.top)*2/3)) {
return;
}
setHoverIndex(index);
item.hoverIndex = index;
},
});
const [, drag] = useDrag({
type: 'row',
item: () => {
return {index};
},
end: (item)=>{
// Time to actually perform the action
setHoverIndex(null);
if(item.hoverIndex >= 0) {
moveRow(item.index, item.hoverIndex);
}
}
});
/* External deps values are from top schema sess data */
depsMap = depsMap.concat(externalDeps.map((source)=>_.get(schemaRef.current.top?.sessData, source)));
depsMap = depsMap.concat([totalRows, row.isExpanded, key, isResizing]);
depsMap = depsMap.concat([totalRows, row.isExpanded, key, isResizing, isHovered]);
drag(dragHandleRef);
drop(rowRef);
return useMemo(()=>
<div {...row.getRowProps()} className="tr">
{row.cells.map((cell, ci) => {
let classNames = [classes.tableCell];
if(typeof(cell.column.id) == 'string' && cell.column.id.startsWith('btn-')) {
classNames.push(classes.btnCell);
}
if(cell.column.id == 'btn-edit' && row.isExpanded) {
classNames.push(classes.expandedIconCell);
}
return (
<div key={ci} {...cell.getCellProps()} className={clsx(classNames)}>
{cell.render('Cell', {
reRenderRow: ()=>{setKey((currKey)=>!currKey);}
})}
</div>
);
})}
</div>, depsMap);
<>
<div {...row.getRowProps()} ref={rowRef} data-handler-id={handlerId}
className={isHovered ? classes.tableRowHovered : null}
>
{row.cells.map((cell, ci) => {
let classNames = [classes.tableCell];
if(typeof(cell.column.id) == 'string' && cell.column.id.startsWith('btn-')) {
classNames.push(classes.btnCell);
}
if(cell.column.id == 'btn-edit' && row.isExpanded) {
classNames.push(classes.expandedIconCell);
}
return (
<div ref={cell.column.id == 'btn-reorder' ? dragHandleRef : null} key={ci} {...cell.getCellProps()} className={clsx(classNames)}>
{cell.render('Cell', {
reRenderRow: ()=>{setKey((currKey)=>!currKey);}
})}
</div>
);
})}
<div className='hover-overlay'></div>
</div>
</>, depsMap);
}
export function DataGridHeader({label, canAdd, onAddClick, canSearch, onSearchTextChange}) {
@ -269,12 +345,33 @@ export default function DataGridView({
const classes = useStyles();
const stateUtils = useContext(StateUtilsContext);
const checkIsMounted = useIsMounted();
const [hoverIndex, setHoverIndex] = useState();
/* Using ref so that schema variable is not frozen in columns closure */
const schemaRef = useRef(schema);
let columns = useMemo(
()=>{
let cols = [];
if(props.canReorder) {
let colInfo = {
Header: <>&nbsp;</>,
id: 'btn-reorder',
accessor: ()=>{/*This is intentional (SonarQube)*/},
disableResizing: true,
sortable: false,
dataType: 'reorder',
width: 26,
minWidth: 26,
maxWidth: 26,
Cell: ()=>{
return <div className={classes.btnReorder}>
<DragIndicatorRoundedIcon fontSize="small" />
</div>;
}
};
colInfo.Cell.displayName = 'Cell';
cols.push(colInfo);
}
if(props.canEdit) {
let colInfo = {
Header: <>&nbsp;</>,
@ -432,7 +529,7 @@ export default function DataGridView({
})
);
return cols;
},[props.canEdit, props.canDelete]
},[props.canEdit, props.canDelete, props.canReorder]
);
const onAddClick = useCallback(()=>{
@ -496,6 +593,15 @@ export default function DataGridView({
}
}, []);
const moveRow = (dragIndex, hoverIndex) => {
dataDispatch({
type: SCHEMA_STATE_ACTIONS.MOVE_ROW,
path: accessPath,
oldIndex: dragIndex,
newIndex: hoverIndex,
});
};
const isResizing = _.flatMap(headerGroups, headerGroup => headerGroup.headers.map(col=>col.isResizing)).includes(true);
if(!props.visible) {
@ -511,23 +617,26 @@ export default function DataGridView({
setGlobalFilter(value || undefined);
}}
/>}
<div {...getTableProps(()=>({style: {minWidth: 'unset'}}))} className={classes.table}>
<DataTableHeader headerGroups={headerGroups} />
<div {...getTableBodyProps()} className={classes.tableContentWidth}>
{rows.map((row, i) => {
prepareRow(row);
return <React.Fragment key={i}>
<DataTableRow row={row} totalRows={rows.length} isResizing={isResizing}
schema={schemaRef.current} schemaRef={schemaRef} accessPath={accessPath.concat([row.index])} />
{props.canEdit && row.isExpanded &&
<FormView value={row.original} viewHelperProps={viewHelperProps} dataDispatch={dataDispatch}
schema={schemaRef.current} accessPath={accessPath.concat([row.index])} isNested={true} className={classes.expandedForm}
isDataGridForm={true}/>
}
</React.Fragment>;
})}
<DndProvider backend={HTML5Backend}>
<div {...getTableProps(()=>({style: {minWidth: 'unset'}}))} className={classes.table}>
<DataTableHeader headerGroups={headerGroups} />
<div {...getTableBodyProps()} className={classes.tableContentWidth}>
{rows.map((row, i) => {
prepareRow(row);
return <React.Fragment key={i}>
<DataTableRow index={i} row={row} totalRows={rows.length} isResizing={isResizing}
schema={schemaRef.current} schemaRef={schemaRef} accessPath={accessPath.concat([row.index])}
moveRow={moveRow} isHovered={i == hoverIndex} setHoverIndex={setHoverIndex} />
{props.canEdit && row.isExpanded &&
<FormView value={row.original} viewHelperProps={viewHelperProps} dataDispatch={dataDispatch}
schema={schemaRef.current} accessPath={accessPath.concat([row.index])} isNested={true} className={classes.expandedForm}
isDataGridForm={true}/>
}
</React.Fragment>;
})}
</div>
</div>
</div>
</DndProvider>
</Box>
</Box>
);
@ -546,6 +655,7 @@ DataGridView.propTypes = {
canEdit: PropTypes.bool,
canAdd: PropTypes.bool,
canDelete: PropTypes.bool,
canReorder: PropTypes.bool,
visible: PropTypes.bool,
canAddRow: PropTypes.oneOfType([
PropTypes.bool, PropTypes.func,

View File

@ -134,13 +134,14 @@ export function getFieldMetaData(field, schema, value, viewHelperProps, onlyMode
retData.editable = evalFunc(schema, _.isUndefined(editable) ? true : editable, value);
}
let {canAdd, canEdit, canDelete, canAddRow } = field;
let {canAdd, canEdit, canDelete, canReorder, canAddRow } = field;
retData.canAdd = _.isUndefined(canAdd) ? retData.canAdd : evalFunc(schema, canAdd, value);
retData.canAdd = !retData.disabled && retData.canAdd;
retData.canEdit = _.isUndefined(canEdit) ? retData.canEdit : evalFunc(schema, canEdit, value);
retData.canEdit = !retData.disabled && retData.canEdit;
retData.canDelete = _.isUndefined(canDelete) ? retData.canDelete : evalFunc(schema, canDelete, value);
retData.canDelete = !retData.disabled && retData.canDelete;
retData.canReorder =_.isUndefined(canReorder) ? retData.canReorder : evalFunc(schema, canReorder, value);
retData.canAddRow = _.isUndefined(canAddRow) ? retData.canAddRow : evalFunc(schema, canAddRow, value);
return retData;
}
@ -214,7 +215,7 @@ export default function FormView({
/* Prepare the array of components based on the types */
for(const field of schemaRef.current.fields) {
let {visible, disabled, readonly, canAdd, canEdit, canDelete, canAddRow, modeSupported} =
let {visible, disabled, readonly, canAdd, canEdit, canDelete, canReorder, canAddRow, modeSupported} =
getFieldMetaData(field, schema, value, viewHelperProps);
if(modeSupported) {
@ -273,7 +274,8 @@ export default function FormView({
const props = {
key: field.id, value: value[field.id] || [], viewHelperProps: viewHelperProps,
schema: field.schema, accessPath: accessPath.concat(field.id), dataDispatch: dataDispatch,
containerClassName: classes.controlRow, ...field, canAdd: canAdd, canEdit: canEdit, canDelete: canDelete,
containerClassName: classes.controlRow, ...field, canAdd: canAdd, canReorder: canReorder,
canEdit: canEdit, canDelete: canDelete,
visible: visible, canAddRow: canAddRow, onDelete: field.onDelete, canSearch: field.canSearch
};

View File

@ -196,18 +196,29 @@ function getChangedData(topSchema, viewHelperProps, sessData, stringify=false, i
}
} else if(!isEdit) {
if(field.type === 'collection') {
const changeDiff = diffArray(
_.get(origVal, field.id) || [],
_.get(sessVal, field.id) || [],
'cid',
);
const origColl = _.get(origVal, field.id) || [];
const sessColl = _.get(sessVal, field.id) || [];
let changeDiff = diffArray(origColl,sessColl,'cid');
/* For fixed rows, check only the updated changes */
/* If canReorder, check the updated changes */
if((!_.isUndefined(field.fixedRows) && changeDiff.updated.length > 0)
|| (_.isUndefined(field.fixedRows) && (
changeDiff.added.length > 0 || changeDiff.removed.length > 0 || changeDiff.updated.length > 0
))) {
))
|| (field.canReorder && _.differenceBy(origColl, sessColl, 'cid'))
) {
let change = cleanCid(_.get(sessVal, field.id), viewHelperProps.keepCid);
attrChanged(field.id, change, true);
return;
}
if(field.canReorder) {
changeDiff = diffArray(origColl,sessColl);
if(changeDiff.updated.length > 0) {
let change = cleanCid(_.get(sessVal, field.id), viewHelperProps.keepCid);
attrChanged(field.id, change, true);
}
}
} else {
attrChanged(field.id);
@ -298,10 +309,11 @@ export const SCHEMA_STATE_ACTIONS = {
SET_VALUE: 'set_value',
ADD_ROW: 'add_row',
DELETE_ROW: 'delete_row',
MOVE_ROW: 'move_row',
RERENDER: 'rerender',
CLEAR_DEFERRED_QUEUE: 'clear_deferred_queue',
DEFERRED_DEPCHANGE: 'deferred_depchange',
BULK_UPDATE: 'bulk_update'
BULK_UPDATE: 'bulk_update',
};
const getDepChange = (currPath, newState, oldState, action)=>{
@ -384,6 +396,13 @@ const sessDataReducer = (state, action)=>{
/* If there is any dep listeners get the changes */
data = getDepChange(action.path, data, state, action);
break;
case SCHEMA_STATE_ACTIONS.MOVE_ROW:
rows = _.get(data, action.path)||[];
var row = rows[action.oldIndex];
rows.splice(action.oldIndex, 1);
rows.splice(action.newIndex, 0, row);
_.set(data, action.path, rows);
break;
case SCHEMA_STATE_ACTIONS.CLEAR_DEFERRED_QUEUE:
data.__deferred__ = [];
break;

View File

@ -430,7 +430,12 @@ module.exports = [{
plugins: ['@babel/plugin-proposal-class-properties', '@babel/proposal-object-rest-spread'],
},
},
}, {
},{
test: /\.m?js$/,
resolve: {
fullySpecified: false
},
},{
test: /\.tsx?$|\.ts?$/,
use: {
loader: 'babel-loader',

View File

@ -107,6 +107,11 @@ module.exports = {
},
enforce: 'post',
exclude: /node_modules|plugins|bundle|generated|regression|[Tt]est.js|[Ss]pecs.js|[Ss]pec.js|\.spec\.js/,
},{
test: /\.m?js$/,
resolve: {
fullySpecified: false
},
},
],
},

View File

@ -1791,6 +1791,13 @@
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.9.2":
version "7.19.0"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.19.0.tgz#22b11c037b094d27a8a2504ea4dcff00f50e2259"
integrity sha512-eR8Lo9hnDS7tqkO7NsV+mKvCmv5boaXFSZ70DnfhcgiEne8hv9oCEd36Klw74EtizEqLsy4YnW8UWwpBVolHZA==
dependencies:
regenerator-runtime "^0.13.4"
"@babel/template@^7.12.13":
version "7.14.5"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.14.5.tgz#a9bc9d8b33354ff6e55a9c60d1109200a68974f4"
@ -2351,6 +2358,21 @@
"@projectstorm/react-diagrams-defaults" "^6.6.1"
"@projectstorm/react-diagrams-routing" "^6.6.1"
"@react-dnd/asap@^5.0.1":
version "5.0.2"
resolved "https://registry.yarnpkg.com/@react-dnd/asap/-/asap-5.0.2.tgz#1f81f124c1cd6f39511c11a881cfb0f715343488"
integrity sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A==
"@react-dnd/invariant@^4.0.1":
version "4.0.2"
resolved "https://registry.yarnpkg.com/@react-dnd/invariant/-/invariant-4.0.2.tgz#b92edffca10a26466643349fac7cdfb8799769df"
integrity sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw==
"@react-dnd/shallowequal@^4.0.1":
version "4.0.2"
resolved "https://registry.yarnpkg.com/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz#d1b4befa423f692fa4abf1c79209702e7d8ae4b4"
integrity sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==
"@react-leaflet/core@^1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@react-leaflet/core/-/core-1.1.1.tgz#827fd05bb542cf874116176d8ef48d5b12163f81"
@ -4902,6 +4924,15 @@ discontinuous-range@1.0.0:
resolved "https://registry.yarnpkg.com/discontinuous-range/-/discontinuous-range-1.0.0.tgz#e38331f0844bba49b9a9cb71c771585aab1bc65a"
integrity sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ==
dnd-core@^16.0.1:
version "16.0.1"
resolved "https://registry.yarnpkg.com/dnd-core/-/dnd-core-16.0.1.tgz#a1c213ed08961f6bd1959a28bb76f1a868360d19"
integrity sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==
dependencies:
"@react-dnd/asap" "^5.0.1"
"@react-dnd/invariant" "^4.0.1"
redux "^4.2.0"
doctrine@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d"
@ -9170,7 +9201,7 @@ process-nextick-args@~2.0.0:
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
process@^0.11.10, process@~0.11.0:
process@~0.11.0:
version "0.11.10"
resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==
@ -9521,6 +9552,24 @@ react-checkbox-tree@^1.7.2:
dependencies:
clsx "^1.1.1"
react-dnd-html5-backend@^16.0.1:
version "16.0.1"
resolved "https://registry.yarnpkg.com/react-dnd-html5-backend/-/react-dnd-html5-backend-16.0.1.tgz#87faef15845d512a23b3c08d29ecfd34871688b6"
integrity sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==
dependencies:
dnd-core "^16.0.1"
react-dnd@^16.0.1:
version "16.0.1"
resolved "https://registry.yarnpkg.com/react-dnd/-/react-dnd-16.0.1.tgz#2442a3ec67892c60d40a1559eef45498ba26fa37"
integrity sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==
dependencies:
"@react-dnd/invariant" "^4.0.1"
"@react-dnd/shallowequal" "^4.0.1"
dnd-core "^16.0.1"
fast-deep-equal "^3.1.3"
hoist-non-react-statics "^3.3.2"
react-dom@^16.6.3:
version "16.14.0"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.14.0.tgz#7ad838ec29a777fb3c75c3a190f661cf92ab8b89"
@ -9784,6 +9833,13 @@ redent@^4.0.0:
indent-string "^5.0.0"
strip-indent "^4.0.0"
redux@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.0.tgz#46f10d6e29b6666df758780437651eeb2b969f13"
integrity sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA==
dependencies:
"@babel/runtime" "^7.9.2"
reflect.ownkeys@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460"