From feb3093c6d0d82690adf138e36f4c2e25cac4564 Mon Sep 17 00:00:00 2001 From: Aditya Toshniwal Date: Thu, 27 Jun 2024 13:21:18 +0530 Subject: [PATCH] Automatically apply virtualization in the DataGridView of SchemaView if the schema contains only one collection. #7607 --- .../static/js/SchemaView/DataGridView.jsx | 94 ++++++++++++------- web/pgadmin/static/js/SchemaView/FormView.jsx | 26 ++++- .../static/js/UserManagementDialog.jsx | 2 +- .../SchemaView/SchemaDialogView.spec.js | 4 +- .../javascript/SchemaView/TestSchema.ui.js | 2 +- web/regression/javascript/setup-jest.js | 28 ++++++ 6 files changed, 116 insertions(+), 40 deletions(-) diff --git a/web/pgadmin/static/js/SchemaView/DataGridView.jsx b/web/pgadmin/static/js/SchemaView/DataGridView.jsx index 236770030..576936820 100644 --- a/web/pgadmin/static/js/SchemaView/DataGridView.jsx +++ b/web/pgadmin/static/js/SchemaView/DataGridView.jsx @@ -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({ - - {rows.map((row, i) => { - return - + {virtualizer.getVirtualItems().map((virtualRow) => { + const row = rows[virtualRow.index]; + + return virtualizer.measureElement(node)} + style={{ + transform: `translateY(${virtualRow.start}px)`, // this should always be a `style` as it changes on scroll + }}> + {props.canEdit && diff --git a/web/pgadmin/static/js/SchemaView/FormView.jsx b/web/pgadmin/static/js/SchemaView/FormView.jsx index bf49f2b21..6d3b44896 100644 --- a/web/pgadmin/static/js/SchemaView/FormView.jsx +++ b/web/pgadmin/static/js/SchemaView/FormView.jsx @@ -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({ ); } 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 ( - {Object.keys(finalTabs).map((tabName)=>{ return ( diff --git a/web/pgadmin/tools/user_management/static/js/UserManagementDialog.jsx b/web/pgadmin/tools/user_management/static/js/UserManagementDialog.jsx index edbc53fc9..bd0330b1f 100644 --- a/web/pgadmin/tools/user_management/static/js/UserManagementDialog.jsx +++ b/web/pgadmin/tools/user_management/static/js/UserManagementDialog.jsx @@ -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']; diff --git a/web/regression/javascript/SchemaView/SchemaDialogView.spec.js b/web/regression/javascript/SchemaView/SchemaDialogView.spec.js index 7c6dece33..a887eb484 100644 --- a/web/regression/javascript/SchemaView/SchemaDialogView.spec.js +++ b/web/regression/javascript/SchemaView/SchemaDialogView.spec.js @@ -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'); }; diff --git a/web/regression/javascript/SchemaView/TestSchema.ui.js b/web/regression/javascript/SchemaView/TestSchema.ui.js index 7b7af9293..d5eb8ea98 100644 --- a/web/regression/javascript/SchemaView/TestSchema.ui.js +++ b/web/regression/javascript/SchemaView/TestSchema.ui.js @@ -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, diff --git a/web/regression/javascript/setup-jest.js b/web/regression/javascript/setup-jest.js index be98ea2db..cd90d801e 100644 --- a/web/regression/javascript/setup-jest.js +++ b/web/regression/javascript/setup-jest.js @@ -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