From 97ae3a7271bf1a9608b4a62adbfd280b66114a49 Mon Sep 17 00:00:00 2001 From: Tim Chamberlain Date: Fri, 8 Dec 2023 12:01:16 -0600 Subject: [PATCH 1/2] refactored mouse events on tables into DataGridUtils to fix text selection on child tables. --- .../widgets/misc/RecordGridWidget.tsx | 40 ++++++++++++++++--- src/qqq/pages/records/query/RecordQuery.tsx | 34 +++------------- src/qqq/utils/DataGridUtils.tsx | 36 +++++++++++++++-- 3 files changed, 73 insertions(+), 37 deletions(-) diff --git a/src/qqq/components/widgets/misc/RecordGridWidget.tsx b/src/qqq/components/widgets/misc/RecordGridWidget.tsx index 99a8b2c..330a130 100644 --- a/src/qqq/components/widgets/misc/RecordGridWidget.tsx +++ b/src/qqq/components/widgets/misc/RecordGridWidget.tsx @@ -27,8 +27,8 @@ import Button from "@mui/material/Button"; import Icon from "@mui/material/Icon"; import Tooltip from "@mui/material/Tooltip/Tooltip"; import Typography from "@mui/material/Typography"; -import {DataGridPro, GridCallbackDetails, GridRowParams, MuiEvent} from "@mui/x-data-grid-pro"; -import React, {useEffect, useState} from "react"; +import {DataGridPro, GridCallbackDetails, GridEventListener, GridFilterModel, gridPreferencePanelStateSelector, GridRowParams, GridSelectionModel, GridToolbarColumnsButton, GridToolbarContainer, GridToolbarDensitySelector, GridToolbarExportContainer, GridToolbarFilterButton, MuiEvent, useGridApiContext, useGridApiEventHandler, useGridSelector} from "@mui/x-data-grid-pro"; +import React, {useEffect, useRef, useState} from "react"; import {useNavigate, Link} from "react-router-dom"; import Widget, {AddNewRecordButton, LabelComponent} from "qqq/components/widgets/Widget"; import DataGridUtils from "qqq/utils/DataGridUtils"; @@ -48,12 +48,15 @@ const qController = Client.getInstance(); function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element { + const instance = useRef({timer: null}); const [rows, setRows] = useState([]); const [records, setRecords] = useState([] as QRecord[]) const [columns, setColumns] = useState([]); const [allColumns, setAllColumns] = useState([]) const [csv, setCsv] = useState(null as string); const [fileName, setFileName] = useState(null as string); + const [gridMouseDownX, setGridMouseDownX] = useState(0); + const [gridMouseDownY, setGridMouseDownY] = useState(0); const navigate = useNavigate(); useEffect(() => @@ -196,19 +199,44 @@ function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element } + ///////////////////////////////////////////////////////////////// + // if a grid preference window is open, ignore and reset timer // + ///////////////////////////////////////////////////////////////// const handleRowClick = (params: GridRowParams, event: MuiEvent, details: GridCallbackDetails) => { (async () => { const qInstance = await qController.loadMetaData() - const tablePath = qInstance.getTablePathByName(data.childTableMetaData.name) + let tablePath = qInstance.getTablePathByName(data.childTableMetaData.name) if(tablePath) { - navigate(`${tablePath}/${params.id}`); + tablePath = `${tablePath}/${params.id}`; + DataGridUtils.handleRowClick(tablePath, event, gridMouseDownX, gridMouseDownY, navigate, instance); } })(); }; + function CustomToolbar() + { + const handleMouseDown: GridEventListener<"cellMouseDown"> = ( params, event, details ) => + { + setGridMouseDownX(event.clientX); + setGridMouseDownY(event.clientY); + clearTimeout(instance.current.timer); + }; + + const handleDoubleClick: GridEventListener<"rowDoubleClick"> = (event: any) => + { + clearTimeout(instance.current.timer); + }; + + const apiRef = useGridApiContext(); + useGridApiEventHandler(apiRef, "cellMouseDown", handleMouseDown); + useGridApiEventHandler(apiRef, "rowDoubleClick", handleDoubleClick); + + return (); + } + return ( (params.indexRelativeToCurrentPage % 2 === 0 ? "even" : "odd")} onRowClick={handleRowClick} // getRowHeight={() => "auto"} // maybe nice? wraps values in cells... - // components={{Toolbar: CustomToolbar, Pagination: CustomPagination, LoadingOverlay: Loading}} + components={{ + Toolbar: CustomToolbar + }} // pinnedColumns={pinnedColumns} // onPinnedColumnsChange={handlePinnedColumnsChange} // pagination diff --git a/src/qqq/pages/records/query/RecordQuery.tsx b/src/qqq/pages/records/query/RecordQuery.tsx index 4510fe0..b32c3e9 100644 --- a/src/qqq/pages/records/query/RecordQuery.tsx +++ b/src/qqq/pages/records/query/RecordQuery.tsx @@ -903,37 +903,13 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element return; } - /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // strategy for when to trigger or not trigger a row click: // - // To avoid a drag-event that highlighted text in a cell: // - // - we capture the x & y upon mouse-down - then compare them in this method (which fires when the mouse is up) // - // if they are more than 5 pixels away from the mouse-down, then assume it's a drag, not a click. // - // - avoid clicking the row upon double-click, by setting a 500ms timer here - and in the onDoubleClick handler, // - // cancelling the timer. // - // - also avoid a click, then click-again-and-start-dragging, by always cancelling the timer in mouse-down. // - // All in, these seem to have good results - the only downside being the half-second delay... // - /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - const diff = Math.max(Math.abs(event.clientX - gridMouseDownX), Math.abs(event.clientY - gridMouseDownY)); - if (diff < 5) + let id = encodeURIComponent(params.id); + if (table.primaryKeyField !== "id") { - console.log("clearing timeout"); - clearTimeout(instance.current.timer); - instance.current.timer = setTimeout(() => - { - if (table.primaryKeyField !== "id") - { - navigate(`${metaData.getTablePathByName(tableName)}/${encodeURIComponent(params.row[tableMetaData.primaryKeyField])}`); - } - else - { - navigate(`${metaData.getTablePathByName(tableName)}/${encodeURIComponent(params.id)}`); - } - }, 100); - } - else - { - console.log(`row-click mouse-up happened ${diff} x or y pixels away from the mouse-down - so not considering it a click.`); + id = encodeURIComponent(params.row[tableMetaData.primaryKeyField]); } + const tablePath = `${metaData.getTablePathByName(table.name)}/${id}`; + DataGridUtils.handleRowClick(tablePath, event, gridMouseDownX, gridMouseDownY, navigate, instance); }; diff --git a/src/qqq/utils/DataGridUtils.tsx b/src/qqq/utils/DataGridUtils.tsx index 7ca67da..3dd15ba 100644 --- a/src/qqq/utils/DataGridUtils.tsx +++ b/src/qqq/utils/DataGridUtils.tsx @@ -25,10 +25,10 @@ import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QField import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance"; import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData"; import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord"; -import {GridColDef, GridFilterItem, GridRowsProp} from "@mui/x-data-grid-pro"; +import {GridCallbackDetails, GridColDef, GridFilterItem, GridRowParams, GridRowsProp, MuiEvent} from "@mui/x-data-grid-pro"; import {GridFilterOperator} from "@mui/x-data-grid/models/gridFilterOperator"; -import React from "react"; -import {Link} from "react-router-dom"; +import React, {useRef, useState} from "react"; +import {Link, NavigateFunction, useNavigate} from "react-router-dom"; import {buildQGridPvsOperators, QGridBlobOperators, QGridBooleanOperators, QGridNumericOperators, QGridStringOperators} from "qqq/pages/records/query/GridFilterOperators"; import ValueUtils from "qqq/utils/qqq/ValueUtils"; @@ -81,6 +81,36 @@ const QGridDateTimeOperators = [ export default class DataGridUtils { + /******************************************************************************* + ** + *******************************************************************************/ + public static handleRowClick = (path: string, event: MuiEvent, gridMouseDownX: number, gridMouseDownY: number, navigate: NavigateFunction, instance: any) => + { + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // strategy for when to trigger or not trigger a row click: // + // To avoid a drag-event that highlighted text in a cell: // + // - we capture the x & y upon mouse-down - then compare them in this method (which fires when the mouse is up) // + // if they are more than 5 pixels away from the mouse-down, then assume it's a drag, not a click. // + // - avoid clicking the row upon double-click, by setting a 500ms timer here - and in the onDoubleClick handler, // + // cancelling the timer. // + // - also avoid a click, then click-again-and-start-dragging, by always cancelling the timer in mouse-down. // + // All in, these seem to have good results - the only downside being the half-second delay... // + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + const diff = Math.max(Math.abs(event.clientX - gridMouseDownX), Math.abs(event.clientY - gridMouseDownY)); + if (diff < 5) + { + console.log("clearing timeout"); + clearTimeout(instance.current.timer); + instance.current.timer = setTimeout(() => + { + navigate(path); + }, 100); + } + else + { + console.log(`row-click mouse-up happened ${diff} x or y pixels away from the mouse-down - so not considering it a click.`); + } + } /******************************************************************************* ** From 9db6951584007f99db0b72a7921b2226a84c8727 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 18 Dec 2023 10:33:03 -0600 Subject: [PATCH 2/2] Add comment on CustomToolbar --- src/qqq/components/widgets/misc/RecordGridWidget.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/qqq/components/widgets/misc/RecordGridWidget.tsx b/src/qqq/components/widgets/misc/RecordGridWidget.tsx index 330a130..363a8a4 100644 --- a/src/qqq/components/widgets/misc/RecordGridWidget.tsx +++ b/src/qqq/components/widgets/misc/RecordGridWidget.tsx @@ -216,6 +216,12 @@ function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element })(); }; + /******************************************************************************* + ** So that we can useGridApiContext to add event handlers for mouse down and + ** row double-click (to make it so you don't accidentally click into records), + ** we have to define a grid component, so even though we don't want a custom + ** toolbar, that's why we have this (and why it returns empty) + *******************************************************************************/ function CustomToolbar() { const handleMouseDown: GridEventListener<"cellMouseDown"> = ( params, event, details ) =>