Automatically apply virtualization in the DataGridView of SchemaView if the schema contains only one collection. #7607

This commit is contained in:
Aditya Toshniwal 2024-06-27 13:21:18 +05:30 committed by GitHub
parent 6f96f67655
commit feb3093c6d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 116 additions and 40 deletions

View File

@ -43,11 +43,16 @@ import { InputText } from '../components/FormComponents';
import { usePgAdmin } from '../BrowserComponent';
import { requestAnimationAndFocus } from '../utils';
import { PgReactTable, PgReactTableBody, PgReactTableCell, PgReactTableHeader, PgReactTableRow, PgReactTableRowContent, PgReactTableRowExpandContent } from '../components/PgReactTableStyled';
import { useVirtualizer } from '@tanstack/react-virtual';
const StyledBox = styled(Box)(({theme}) => ({
'& .DataGridView-grid': {
...theme.mixins.panelBorder,
backgroundColor: theme.palette.background.default,
display: 'flex',
flexDirection: 'column',
minHeight: 0,
height: '100%',
'& .DataGridView-gridHeader': {
display: 'flex',
...theme.mixins.panelBorder.bottom,
@ -69,7 +74,6 @@ const StyledBox = styled(Box)(({theme}) => ({
'&.pgrt-table': {
'& .pgrt-body':{
'& .pgrt-row': {
position: 'unset',
backgroundColor: theme.otherVars.emptySpaceBg,
'& .pgrt-row-content':{
'& .pgrd-row-cell': {
@ -174,9 +178,6 @@ function DataTableRow({index, row, totalRows, isResizing, isHovered, schema, sch
});
});
// Try autofocus on newly added row.
requestAnimationAndFocus(rowRef.current?.querySelector('input'));
return ()=>{
/* Cleanup the listeners when unmounting */
depListener?.removeDepListener(accessPath);
@ -494,28 +495,6 @@ export default function DataGridView({
},[props.canEdit, props.canDelete, props.canReorder]
);
const onAddClick = useCallback(()=>{
if(!props.canAddRow) {
return;
}
let newRow = schemaRef.current.getNewData();
const current_macros = schemaRef.current?._top?._sessData?.macro || null;
if (current_macros){
newRow = schemaRef.current.getNewData(current_macros);
}
if(props.expandEditOnAdd && props.canEdit) {
newRowIndex.current = rows.length;
}
dataDispatch({
type: SCHEMA_STATE_ACTIONS.ADD_ROW,
path: accessPath,
value: newRow,
addOnTop: props.addOnTop
});
}, [props.canAddRow, rows?.length]);
const columnVisibility = useMemo(()=>{
const ret = {};
@ -544,6 +523,27 @@ export default function DataGridView({
const rows = table.getRowModel().rows;
const onAddClick = useCallback(()=>{
if(!props.canAddRow) {
return;
}
let newRow = schemaRef.current.getNewData();
const current_macros = schemaRef.current?._top?._sessData?.macro || null;
if (current_macros){
newRow = schemaRef.current.getNewData(current_macros);
}
newRowIndex.current = props.addOnTop ? 0 : rows.length;
dataDispatch({
type: SCHEMA_STATE_ACTIONS.ADD_ROW,
path: accessPath,
value: newRow,
addOnTop: props.addOnTop
});
}, [props.canAddRow, rows?.length]);
useEffect(()=>{
let rowsPromise = fixedRows;
@ -564,8 +564,17 @@ export default function DataGridView({
useEffect(()=>{
if(newRowIndex.current >= 0) {
rows[newRowIndex.current]?.toggleExpanded(true);
newRowIndex.current = null;
virtualizer.scrollToIndex(newRowIndex.current);
// Try autofocus on newly added row.
setTimeout(()=>{
const rowInput = tableRef.current.querySelector(`.pgrt-row[data-index="${newRowIndex.current}"] input`);
if(!rowInput) return;
requestAnimationAndFocus(tableRef.current.querySelector(`.pgrt-row[data-index="${newRowIndex.current}"] input`));
props.expandEditOnAdd && props.canEdit && rows[newRowIndex.current]?.toggleExpanded(true);
newRowIndex.current = undefined;
}, 50);
}
}, [rows?.length]);
@ -582,6 +591,18 @@ export default function DataGridView({
const isResizing = _.flatMap(table.getHeaderGroups(), headerGroup => headerGroup.headers.map(header=>header.column.getIsResizing())).includes(true);
const virtualizer = useVirtualizer({
count: rows.length,
getScrollElement: () => tableRef.current,
estimateSize: () => 42,
measureElement:
typeof window !== 'undefined' &&
navigator.userAgent.indexOf('Firefox') === -1
? element => element?.getBoundingClientRect().height
: undefined,
overscan: viewHelperProps.virtualiseOverscan ?? 10,
});
if(!props.visible) {
return <></>;
}
@ -598,12 +619,19 @@ export default function DataGridView({
<DndProvider backend={HTML5Backend}>
<PgReactTable ref={tableRef} table={table} data-test="data-grid-view" tableClassName='DataGridView-table'>
<PgReactTableHeader table={table} />
<PgReactTableBody>
{rows.map((row, i) => {
return <PgReactTableRow key={row.index}>
<DataTableRow index={i} row={row} totalRows={rows.length} isResizing={isResizing}
<PgReactTableBody style={{ height: virtualizer.getTotalSize() + 'px'}}>
{virtualizer.getVirtualItems().map((virtualRow) => {
const row = rows[virtualRow.index];
return <PgReactTableRow key={row.id} data-index={virtualRow.index}
ref={node => virtualizer.measureElement(node)}
style={{
transform: `translateY(${virtualRow.start}px)`, // this should always be a `style` as it changes on scroll
}}>
<DataTableRow index={virtualRow.index} row={row} totalRows={rows.length} isResizing={isResizing}
schema={schemaRef.current} schemaRef={schemaRef} accessPath={accessPath.concat([row.index])}
moveRow={moveRow} isHovered={i == hoverIndex} setHoverIndex={setHoverIndex} viewHelperProps={viewHelperProps}
moveRow={moveRow} isHovered={virtualRow.index == hoverIndex} setHoverIndex={setHoverIndex} viewHelperProps={viewHelperProps}
measureElement={virtualizer.measureElement}
/>
{props.canEdit &&
<PgReactTableRowExpandContent row={row}>

View File

@ -7,7 +7,7 @@
//
//////////////////////////////////////////////////////////////
import React, { useContext, useEffect, useRef, useState } from 'react';
import React, { useContext, useEffect, useMemo, useRef, useState } from 'react';
import { styled } from '@mui/material/styles';
import { Box, Tab, Tabs, Grid } from '@mui/material';
import _ from 'lodash';
@ -60,6 +60,15 @@ const StyledBox = styled(Box)(({theme}) => ({
},
}
},
'& .FormView-singleCollectionPanel': {
...theme.mixins.tabPanel,
'& .FormView-singleCollectionPanelContent': {
'& .FormView-controlRow': {
marginBottom: theme.spacing(1),
height: '100%',
},
}
},
}));
/* Optional SQL tab */
@ -423,6 +432,17 @@ export default function FormView({
onTabChange?.(tabValue, Object.keys(tabs)[tabValue], sqlTabActive);
}, [tabValue]);
const isSingleCollection = useMemo(()=>{
// we can check if it is a single-collection.
// in that case, we could force virtualization of the collection.
if(isTabView) return false;
const visibleEle = Object.values(finalTabs)[0].filter((c)=>c.props.visible);
return visibleEle.length == 1
&& visibleEle[0]?.type == DataGridView;
}, [isTabView, finalTabs]);
/* check whether form is kept hidden by visible prop */
if(!_.isUndefined(visible) && !visible) {
return <></>;
@ -463,10 +483,10 @@ export default function FormView({
</StyledBox>
);
} else {
let contentClassName = ['FormView-nonTabPanelContent', (stateUtils.formErr.message ? 'FormView-errorMargin' : null)];
let contentClassName = [isSingleCollection ? 'FormView-singleCollectionPanelContent' : 'FormView-nonTabPanelContent', (stateUtils.formErr.message ? 'FormView-errorMargin' : null)];
return (
<StyledBox height="100%" display="flex" flexDirection="column" className={className} ref={formRef} data-test="form-view">
<TabPanel value={tabValue} index={0} classNameRoot={['FormView-nonTabPanel',className].join(' ')}
<TabPanel value={tabValue} index={0} classNameRoot={[isSingleCollection ? 'FormView-singleCollectionPanel' : 'FormView-nonTabPanel',className].join(' ')}
className={contentClassName.join(' ')}>
{Object.keys(finalTabs).map((tabName)=>{
return (

View File

@ -261,7 +261,7 @@ class UserManagementSchema extends BaseUISchema {
return [
{
id: 'userManagement', label: '', type: 'collection', schema: obj.userManagementCollObj,
canAdd: true, canDelete: true, isFullTab: true, group: 'temp_user',
canAdd: true, canDelete: true, isFullTab: true,
addOnTop: true,
canDeleteRow: (row)=>{
return row['id'] != current_user['id'];

View File

@ -70,8 +70,8 @@ describe('SchemaView', ()=>{
await user.type(ctrl.container.querySelector('[name="field2"]'), '2');
await user.type(ctrl.container.querySelector('[name="field5"]'), 'val5');
/* Add a row */
await user.click(ctrl.container.querySelector('[data-test="add-row"]'));
await user.click(ctrl.container.querySelector('[data-test="add-row"]'));
await user.click(ctrl.container.querySelector('button[data-test="add-row"]'));
await user.click(ctrl.container.querySelector('button[data-test="add-row"]'));
await user.type(ctrl.container.querySelectorAll('[name="field5"]')[0], 'rval51');
await user.type(ctrl.container.querySelectorAll('[name="field5"]')[1], 'rval52');
};

View File

@ -35,7 +35,7 @@ class TestSubSchema extends BaseUISchema {
{
id: 'field5', label: 'Field5', type: 'multiline', group: null,
cell: 'text', mode: ['properties', 'edit', 'create'], disabled: false,
noEmpty: true, minWidth: '50%',
noEmpty: true, minWidth: 50,
},
{
id: 'fieldskip', label: 'FieldSkip', type: 'text', group: null,

View File

@ -65,5 +65,33 @@ document.createRange = () => {
return range;
};
// for virtual tables, height should exist.
Element.prototype.getBoundingClientRect = jest.fn(function () {
if (this.classList?.contains('pgrt')) {
return {
width: 400,
height: 400,
top: 0,
left: 0,
bottom: 0,
right: 0,
x: 0,
y: 0,
toJSON: () => {},
};
}
return {
width: 0,
height: 0,
top: 0,
left: 0,
bottom: 0,
right: 0,
x: 0,
y: 0,
toJSON: () => {},
};
});
jest.setTimeout(18000); // 1 second