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
10 changed files with 250 additions and 49 deletions

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;