diff --git a/package.json b/package.json index 3d122a7..cf508cf 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "@auth0/auth0-react": "1.10.2", "@emotion/react": "11.7.1", "@emotion/styled": "11.6.0", - "@kingsrook/qqq-frontend-core": "1.0.57", + "@kingsrook/qqq-frontend-core": "1.0.58", "@mui/icons-material": "5.4.1", "@mui/material": "5.11.1", "@mui/styles": "5.11.1", diff --git a/src/App.tsx b/src/App.tsx index 425d381..98321cd 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -549,7 +549,7 @@ export default function App() }, ); - const [pageHeader, setPageHeader] = useState(""); + const [pageHeader, setPageHeader] = useState("" as string | JSX.Element); const [accentColor, setAccentColor] = useState("#0062FF"); return ( @@ -557,7 +557,7 @@ export default function App() setPageHeader(header), + setPageHeader: (header: string | JSX.Element) => setPageHeader(header), setAccentColor: (accentColor: string) => setAccentColor(accentColor) }}> diff --git a/src/QContext.tsx b/src/QContext.tsx index 673476e..aaf83b2 100644 --- a/src/QContext.tsx +++ b/src/QContext.tsx @@ -24,8 +24,8 @@ import {createContext} from "react"; interface QContext { - pageHeader: string; - setPageHeader?: (header: string) => void; + pageHeader: string | JSX.Element; + setPageHeader?: (header: string | JSX.Element) => void; accentColor: string; setAccentColor?: (header: string) => void; } diff --git a/src/qqq/components/audits/AuditBody.tsx b/src/qqq/components/audits/AuditBody.tsx index e26c99f..90cb0db 100644 --- a/src/qqq/components/audits/AuditBody.tsx +++ b/src/qqq/components/audits/AuditBody.tsx @@ -222,8 +222,8 @@ function AuditBody({tableMetaData, recordId, record}: Props): JSX.Element // if we fetched the limit if (audits.length == limit) { - const count = await qController.count("audit", filter); - setTotal(count); + const [count, distinctCount] = await qController.count("audit", filter, null, true); // todo validate distinct working here! + setTotal(distinctCount); } ////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/src/qqq/components/widgets/misc/RecordGridWidget.tsx b/src/qqq/components/widgets/misc/RecordGridWidget.tsx index fbb1f72..c167468 100644 --- a/src/qqq/components/widgets/misc/RecordGridWidget.tsx +++ b/src/qqq/components/widgets/misc/RecordGridWidget.tsx @@ -60,13 +60,13 @@ function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element } const tableMetaData = new QTableMetaData(data.childTableMetaData); - const {rows, columnsToRender} = DataGridUtils.makeRows(records, tableMetaData); + const rows = DataGridUtils.makeRows(records, tableMetaData); ///////////////////////////////////////////////////////////////////////////////// // note - tablePath may be null, if the user doesn't have access to the table. // ///////////////////////////////////////////////////////////////////////////////// const childTablePath = data.tablePath ? data.tablePath + (data.tablePath.endsWith("/") ? "" : "/") : data.tablePath; - const columns = DataGridUtils.setupGridColumns(tableMetaData, columnsToRender, childTablePath); + const columns = DataGridUtils.setupGridColumns(tableMetaData, childTablePath); //////////////////////////////////////////////////////////////// // do not not show the foreign-key column of the parent table // diff --git a/src/qqq/pages/apps/Home.tsx b/src/qqq/pages/apps/Home.tsx index 055f6b7..db4f326 100644 --- a/src/qqq/pages/apps/Home.tsx +++ b/src/qqq/pages/apps/Home.tsx @@ -127,7 +127,7 @@ function AppHome({app}: Props): JSX.Element let countResult = null; if(tableMetaData.capabilities.has(Capability.TABLE_COUNT) && tableMetaData.readPermission) { - countResult = await qController.count(table.name); + [countResult] = await qController.count(table.name); if (countResult !== null && countResult !== undefined) { diff --git a/src/qqq/pages/records/query/ColumnStats.tsx b/src/qqq/pages/records/query/ColumnStats.tsx index c42c91e..36f4dd5 100644 --- a/src/qqq/pages/records/query/ColumnStats.tsx +++ b/src/qqq/pages/records/query/ColumnStats.tsx @@ -126,8 +126,8 @@ function ColumnStats({tableMetaData, fieldMetaData, filter}: Props): JSX.Element fakeTableMetaData.sections = [] as QTableSection[]; fakeTableMetaData.sections.push(new QTableSection({fieldNames: [fieldMetaData.name, "count"]})); - const {rows, columnsToRender} = DataGridUtils.makeRows(valueCounts, fakeTableMetaData); - const columns = DataGridUtils.setupGridColumns(fakeTableMetaData, columnsToRender); + const rows = DataGridUtils.makeRows(valueCounts, fakeTableMetaData); + const columns = DataGridUtils.setupGridColumns(fakeTableMetaData); columns.forEach((c) => { c.width = 200; diff --git a/src/qqq/pages/records/query/RecordQuery.tsx b/src/qqq/pages/records/query/RecordQuery.tsx index 7bdce1f..1aba942 100644 --- a/src/qqq/pages/records/query/RecordQuery.tsx +++ b/src/qqq/pages/records/query/RecordQuery.tsx @@ -27,6 +27,7 @@ import {QJobComplete} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJo import {QJobError} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobError"; import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord"; import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter"; +import {QueryJoin} from "@kingsrook/qqq-frontend-core/lib/model/query/QueryJoin"; import {Alert, Collapse, TablePagination} from "@mui/material"; import Box from "@mui/material/Box"; import Button from "@mui/material/Button"; @@ -48,7 +49,7 @@ import Stack from "@mui/material/Stack"; import TextField from "@mui/material/TextField"; import Tooltip from "@mui/material/Tooltip"; import Typography from "@mui/material/Typography"; -import {DataGridPro, GridCallbackDetails, GridColDef, gridColumnGroupingSelector, GridColumnMenuContainer, GridColumnMenuProps, GridColumnOrderChangeParams, GridColumnPinningMenuItems, GridColumnsMenuItem, GridColumnVisibilityModel, GridDensity, GridEventListener, GridExportMenuItemProps, GridFilterMenuItem, GridFilterModel, GridPinnedColumns, gridPreferencePanelStateSelector, GridRowId, GridRowParams, GridRowsProp, GridSelectionModel, GridSortItem, GridSortModel, GridState, GridToolbarColumnsButton, GridToolbarContainer, GridToolbarDensitySelector, GridToolbarExportContainer, GridToolbarFilterButton, HideGridColMenuItem, MuiEvent, SortGridMenuItems, useGridApiContext, useGridApiEventHandler, useGridSelector} from "@mui/x-data-grid-pro"; +import {DataGridPro, GridCallbackDetails, GridColDef, GridColumnMenuContainer, GridColumnMenuProps, GridColumnOrderChangeParams, GridColumnPinningMenuItems, GridColumnsMenuItem, GridColumnVisibilityModel, GridDensity, GridEventListener, GridExportMenuItemProps, GridFilterMenuItem, GridFilterModel, GridPinnedColumns, gridPreferencePanelStateSelector, GridRowId, GridRowParams, GridRowsProp, GridSelectionModel, GridSortItem, GridSortModel, GridState, GridToolbarColumnsButton, GridToolbarContainer, GridToolbarDensitySelector, GridToolbarExportContainer, GridToolbarFilterButton, HideGridColMenuItem, MuiEvent, SortGridMenuItems, useGridApiContext, useGridApiEventHandler, useGridSelector} from "@mui/x-data-grid-pro"; import {GridColumnsPanelProps} from "@mui/x-data-grid/components/panel/GridColumnsPanel"; import {gridColumnDefinitionsSelector, gridColumnVisibilityModelSelector} from "@mui/x-data-grid/hooks/features/columns/gridColumnsSelector"; import FormData from "form-data"; @@ -57,6 +58,7 @@ import {useLocation, useNavigate, useSearchParams} from "react-router-dom"; import QContext from "QContext"; import {QActionsMenuButton, QCancelButton, QCreateNewButton} from "qqq/components/buttons/DefaultButtons"; import SavedFilters from "qqq/components/misc/SavedFilters"; +import CustomWidthTooltip from "qqq/components/tooltips/CustomWidthTooltip"; import BaseLayout from "qqq/layouts/BaseLayout"; import ProcessRun from "qqq/pages/processes/ProcessRun"; import ColumnStats from "qqq/pages/records/query/ColumnStats"; @@ -90,10 +92,10 @@ const qController = Client.getInstance(); function RecordQuery({table, launchProcess}: Props): JSX.Element { const tableName = table.name; - const [ searchParams ] = useSearchParams(); + const [searchParams] = useSearchParams(); const [showSuccessfullyDeletedAlert, setShowSuccessfullyDeletedAlert] = useState(searchParams.has("deleteSuccess")); - const [successAlert, setSuccessAlert] = useState(null as string) + const [successAlert, setSuccessAlert] = useState(null as string); const location = useLocation(); const navigate = useNavigate(); @@ -111,6 +113,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element const filterLocalStorageKey = `${FILTER_LOCAL_STORAGE_KEY_ROOT}.${tableName}`; let defaultSort = [] as GridSortItem[]; let defaultVisibility = {} as { [index: string]: boolean }; + let didDefaultVisibilityComeFromLocalStorage = false; let defaultRowsPerPage = 10; let defaultDensity = "standard" as GridDensity; let defaultPinnedColumns = {left: ["__check__", "id"]} as GridPinnedColumns; @@ -127,6 +130,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element if (localStorage.getItem(columnVisibilityLocalStorageKey)) { defaultVisibility = JSON.parse(localStorage.getItem(columnVisibilityLocalStorageKey)); + didDefaultVisibilityComeFromLocalStorage = true; } if (localStorage.getItem(pinnedColumnsLocalStorageKey)) { @@ -144,6 +148,8 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element const [filterModel, setFilterModel] = useState({items: []} as GridFilterModel); const [columnSortModel, setColumnSortModel] = useState(defaultSort); const [columnVisibilityModel, setColumnVisibilityModel] = useState(defaultVisibility); + const [didDefaultVisibilityModelComeFromLocalStorage, setDidDefaultVisibilityModelComeFromLocalStorage] = useState(didDefaultVisibilityComeFromLocalStorage); + const [visibleJoinTables, setVisibleJoinTables] = useState(new Set()); const [rowsPerPage, setRowsPerPage] = useState(defaultRowsPerPage); const [density, setDensity] = useState(defaultDensity); const [pinnedColumns, setPinnedColumns] = useState(defaultPinnedColumns); @@ -157,6 +163,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element const [allTableProcesses, setAllTableProcesses] = useState([] as QProcessMetaData[]); const [pageNumber, setPageNumber] = useState(0); const [totalRecords, setTotalRecords] = useState(null); + const [distinctRecords, setDistinctRecords] = useState(null); const [selectedIds, setSelectedIds] = useState([] as string[]); const [selectFullFilterState, setSelectFullFilterState] = useState("n/a" as "n/a" | "checked" | "filter"); const [columnsModel, setColumnsModel] = useState([] as GridColDef[]); @@ -174,26 +181,26 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element const [activeModalProcess, setActiveModalProcess] = useState(null as QProcessMetaData); const [launchingProcess, setLaunchingProcess] = useState(launchProcess); const [recordIdsForProcess, setRecordIdsForProcess] = useState(null as string | QQueryFilter); - const [columnStatsFieldName, setColumnStatsFieldName] = useState(null as string) - const [filterForColumnStats, setFilterForColumnStats] = useState(null as QQueryFilter) + const [columnStatsFieldName, setColumnStatsFieldName] = useState(null as string); + const [filterForColumnStats, setFilterForColumnStats] = useState(null as QQueryFilter); const instance = useRef({timer: null}); //////////////////////////////////////////////////////////////////////////////////////////////////////////////// // use all these states to avoid showing results from an "old" query, that finishes loading after a newer one // //////////////////////////////////////////////////////////////////////////////////////////////////////////////// - const [ latestQueryId, setLatestQueryId ] = useState(0); - const [ countResults, setCountResults ] = useState({} as any); - const [ receivedCountTimestamp, setReceivedCountTimestamp ] = useState(new Date()); - const [ queryResults, setQueryResults ] = useState({} as any); - const [ latestQueryResults, setLatestQueryResults ] = useState(null as QRecord[]); - const [ receivedQueryTimestamp, setReceivedQueryTimestamp ] = useState(new Date()); - const [ queryErrors, setQueryErrors ] = useState({} as any); - const [ receivedQueryErrorTimestamp, setReceivedQueryErrorTimestamp ] = useState(new Date()); + const [latestQueryId, setLatestQueryId] = useState(0); + const [countResults, setCountResults] = useState({} as any); + const [receivedCountTimestamp, setReceivedCountTimestamp] = useState(new Date()); + const [queryResults, setQueryResults] = useState({} as any); + const [latestQueryResults, setLatestQueryResults] = useState(null as QRecord[]); + const [receivedQueryTimestamp, setReceivedQueryTimestamp] = useState(new Date()); + const [queryErrors, setQueryErrors] = useState({} as any); + const [receivedQueryErrorTimestamp, setReceivedQueryErrorTimestamp] = useState(new Date()); const {setPageHeader} = useContext(QContext); - const [ , forceUpdate ] = useReducer((x) => x + 1, 0); + const [, forceUpdate] = useReducer((x) => x + 1, 0); const openActionsMenu = (event: any) => setActionsMenu(event.currentTarget); const closeActionsMenu = () => setActionsMenu(null); @@ -218,7 +225,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element setActiveModalProcess(processList[0]); return; } - else if(metaData?.processes.has(processName)) + else if (metaData?.processes.has(processName)) { /////////////////////////////////////////////////////////////////////////////////////// // check for generic processes - should this be a specific attribute on the process? // @@ -237,12 +244,12 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element // so if path has '/savedFilter/' get last parsed string // ///////////////////////////////////////////////////////////////////// let currentSavedFilterId = null as number; - if(location.pathname.indexOf("/savedFilter/") != -1) + if (location.pathname.indexOf("/savedFilter/") != -1) { const parts = location.pathname.split("/"); currentSavedFilterId = Number.parseInt(parts[parts.length - 1]); } - else if(!searchParams.has("filter")) + else if (!searchParams.has("filter")) { if (localStorage.getItem(currentSavedFilterLocalStorageKey)) { @@ -255,7 +262,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element } } - if(currentSavedFilterId != null) + if (currentSavedFilterId != null) { (async () => { @@ -296,15 +303,15 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element //////////////////////////////////////////////////////////////////////////////////// setActiveModalProcess(null); - }, [location , tableMetaData]); + }, [location, tableMetaData]); /////////////////////////////////////////////////////////////////////// // any time these are out of sync, it means we need to reload things // /////////////////////////////////////////////////////////////////////// - if(tableMetaData && tableMetaData.name !== tableName) + if (tableMetaData && tableMetaData.name !== tableName) { console.log(" it looks like we changed tables - try to reload the things"); - setTableMetaData(null) + setTableMetaData(null); setColumnSortModel([]); setColumnVisibilityModel({}); setColumnsModel([]); @@ -322,7 +329,90 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element { const filter = FilterUtils.buildQFilterFromGridFilter(tableMetaData, filterModel, columnSortModel); setHasValidFilters(filter.criteria && filter.criteria.length > 0); - return(filter); + return (filter); + }; + + const getVisibleJoinTables = (): Set => + { + const visibleJoinTables = new Set(); + columnsModel.forEach((gridColumn) => + { + const fieldName = gridColumn.field; + if (columnVisibilityModel[fieldName] !== false) + { + if (fieldName.indexOf(".") > -1) + { + visibleJoinTables.add(fieldName.split(".")[0]); + } + } + }); + return (visibleJoinTables); + }; + + const isJoinMany = (tableMetaData: QTableMetaData, visibleJoinTables: Set): boolean => + { + if (tableMetaData?.exposedJoins) + { + for (let i = 0; i < tableMetaData.exposedJoins.length; i++) + { + const join = tableMetaData.exposedJoins[i]; + if (visibleJoinTables.has(join.joinTable.name)) + { + if(join.isMany) + { + return (true); + } + } + } + } + return (false); + } + + const getPageHeader = (tableMetaData: QTableMetaData, visibleJoinTables: Set): string | JSX.Element => + { + if (visibleJoinTables.size > 0) + { + let joinLabels = []; + if (tableMetaData?.exposedJoins) + { + for (let i = 0; i < tableMetaData.exposedJoins.length; i++) + { + const join = tableMetaData.exposedJoins[i]; + if (visibleJoinTables.has(join.joinTable.name)) + { + joinLabels.push(join.label); + } + } + } + + let joinLabelsString = joinLabels.join(", "); + if(joinLabels.length == 2) + { + let lastCommaIndex = joinLabelsString.lastIndexOf(","); + joinLabelsString = joinLabelsString.substring(0, lastCommaIndex) + " and " + joinLabelsString.substring(lastCommaIndex + 1); + } + if(joinLabels.length > 2) + { + let lastCommaIndex = joinLabelsString.lastIndexOf(","); + joinLabelsString = joinLabelsString.substring(0, lastCommaIndex) + ", and " + joinLabelsString.substring(lastCommaIndex + 1); + } + + let tooltipHTML =
+ You are viewing results from the {tableMetaData.label} table joined with the {joinLabelsString} table{joinLabels.length == 1 ? "" : "s"} +
+ + return( +
+ {tableMetaData?.label} + + emergency + +
); + } + else + { + return (tableMetaData?.label); + } }; const updateTable = () => @@ -332,7 +422,29 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element (async () => { const tableMetaData = await qController.loadTableMetaData(tableName); - setPageHeader(tableMetaData.label); + + const visibleJoinTables = getVisibleJoinTables(); + setPageHeader(getPageHeader(tableMetaData, visibleJoinTables)); + + if (!didDefaultVisibilityModelComeFromLocalStorage) + { + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // if we didn't load the column visibility from local storage, then by default, it'll be an empty array, and all fields will be visible. // + // but - if the table has join tables, we don't want them on by default, so, flip them off! // + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + if (tableMetaData?.exposedJoins) + { + for (let i = 0; i < tableMetaData.exposedJoins.length; i++) + { + const join = tableMetaData.exposedJoins[i]; + for (let fieldName of join.joinTable.fields.keys()) + { + columnVisibilityModel[`${join.joinTable.name}.${fieldName}`] = false; + } + } + } + setColumnVisibilityModel(columnVisibilityModel); + } //////////////////////////////////////////////////////////////////////////////////////////////// // we need the table meta data to look up the default filter (if it comes from query string), // @@ -353,14 +465,37 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element setTableMetaData(tableMetaData); setTableLabel(tableMetaData.label); - if(columnsModel.length == 0) + if (columnsModel.length == 0) { - let linkBase = metaData.getTablePath(table) + let linkBase = metaData.getTablePath(table); linkBase += linkBase.endsWith("/") ? "" : "/"; - const columns = DataGridUtils.setupGridColumns(tableMetaData, null, linkBase); + const columns = DataGridUtils.setupGridColumns(tableMetaData, linkBase); setColumnsModel(columns); } + ////////////////////////////////////////////////////////////////////////////////////////////////// + // make sure that any if any sort columns are from a join table, that the join table is visible // + ////////////////////////////////////////////////////////////////////////////////////////////////// + let resetColumnSortModel = false; + for (let i = 0; i < columnSortModel.length; i++) + { + const gridSortItem = columnSortModel[i]; + if (gridSortItem.field.indexOf(".") > -1) + { + const tableName = gridSortItem.field.split(".")[0]; + if (!visibleJoinTables?.has(tableName)) + { + columnSortModel.splice(i, 1); + setColumnSortModel(columnSortModel); + resetColumnSortModel = true; + i--; + } + } + } + + /////////////////////////////////////////////////////////// + // if there's no column sort, make a default - pkey desc // + /////////////////////////////////////////////////////////// if (columnSortModel.length === 0) { columnSortModel.push({ @@ -368,10 +503,48 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element sort: "desc", }); setColumnSortModel(columnSortModel); + resetColumnSortModel = true; + } + + if (resetColumnSortModel && latestQueryId > 0) + { + ///////////////////////////////////////////////////////////////////////////////////////////////////////////// + // let the next render (since columnSortModel is watched below) build the filter, using the new columnSort // + ///////////////////////////////////////////////////////////////////////////////////////////////////////////// + return; } const qFilter = buildQFilter(tableMetaData, localFilterModel); + ////////////////////////////////////////// + // figure out joins to use in the query // + ////////////////////////////////////////// + let queryJoins = null; + if (tableMetaData?.exposedJoins) + { + const visibleJoinTables = getVisibleJoinTables(); + + queryJoins = []; + for (let i = 0; i < tableMetaData.exposedJoins.length; i++) + { + const join = tableMetaData.exposedJoins[i]; + if (visibleJoinTables.has(join.joinTable.name)) + { + queryJoins.push(new QueryJoin(join.joinTable.name, true, "LEFT")); + } + } + } + + /////////////////////////////////////////////////////////////////////////////////////////////////////// + // before we can issue the query, we must have the columns model (to figure out if we need to join). // + // so, if we don't have it, then return and let a later call do it. // + /////////////////////////////////////////////////////////////////////////////////////////////////////// + if(!columnsModel || columnsModel.length == 0) + { + console.log("Returning before issuing query, because no columnsModel."); + return; + } + ////////////////////////////////////////////////////////////////////////////////////////////////// // assign a new query id to the query being issued here. then run both the count & query async // // and when they load, store their results associated with this id. // @@ -382,15 +555,19 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element console.log(`Issuing query: ${thisQueryId}`); if (tableMetaData.capabilities.has(Capability.TABLE_COUNT)) { - qController.count(tableName, qFilter).then((count) => + let includeDistinct = isJoinMany(tableMetaData, getVisibleJoinTables()); + qController.count(tableName, qFilter, queryJoins, includeDistinct).then(([count, distinctCount]) => { - countResults[thisQueryId] = count; + console.log(`Received count results for query ${thisQueryId}: ${count} ${distinctCount}`); + countResults[thisQueryId] = []; + countResults[thisQueryId].push(count); + countResults[thisQueryId].push(distinctCount); setCountResults(countResults); setReceivedCountTimestamp(new Date()); }); } - qController.query(tableName, qFilter, rowsPerPage, pageNumber * rowsPerPage).then((results) => + qController.query(tableName, qFilter, rowsPerPage, pageNumber * rowsPerPage, queryJoins).then((results) => { console.log(`Received results for query ${thisQueryId}`); queryResults[thisQueryId] = results; @@ -430,7 +607,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element /////////////////////////// useEffect(() => { - if (countResults[latestQueryId] === null) + if (countResults[latestQueryId] == null || countResults[latestQueryId].length == 0) { /////////////////////////////////////////////// // see same idea in displaying query results // @@ -438,9 +615,17 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element console.log(`No count results for id ${latestQueryId}...`); return; } - setTotalRecords(countResults[latestQueryId]); - delete countResults[latestQueryId]; - }, [ receivedCountTimestamp ]); + try + { + setTotalRecords(countResults[latestQueryId][0]); + setDistinctRecords(countResults[latestQueryId][1]); + delete countResults[latestQueryId]; + } + catch(e) + { + console.log(e); + } + }, [receivedCountTimestamp]); /////////////////////////// // display query results // @@ -462,14 +647,13 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element delete queryResults[latestQueryId]; setLatestQueryResults(results); - const {rows, columnsToRender} = DataGridUtils.makeRows(results, tableMetaData); - + const rows = DataGridUtils.makeRows(results, tableMetaData); setRows(rows); setLoading(false); setAlertContent(null); forceUpdate(); - }, [ receivedQueryTimestamp ]); + }, [receivedQueryTimestamp]); ///////////////////////// // display query error // @@ -491,7 +675,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element setLoading(false); setAlertContent(errorMessage); - }, [ receivedQueryErrorTimestamp ]); + }, [receivedQueryErrorTimestamp]); const handlePageChange = (page: number) => @@ -550,7 +734,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element clearTimeout(instance.current.timer); instance.current.timer = setTimeout(() => { - if(table.primaryKeyField !== "id") + if (table.primaryKeyField !== "id") { navigate(`${metaData.getTablePathByName(tableName)}/${params.row[tableMetaData.primaryKeyField]}`); } @@ -573,7 +757,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element selectionModel.forEach((value: GridRowId, index: number) => { let valueToPush = value as string; - if(tableMetaData.primaryKeyField !== "id") + if (tableMetaData.primaryKeyField !== "id") { valueToPush = latestQueryResults[index].values.get(tableMetaData.primaryKeyField); } @@ -603,6 +787,19 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element } }; + ///////////////////////////////////////////////////////////////////////////////////////////////////////////// + // if, after a column was turned on or off, the set of visibleJoinTables is changed, then update the table // + // check this on each render - it should only be different if there was a change. note that putting this // + // in handleColumnVisibilityChange "didn't work" - it was always "behind by one" (like, maybe data grid // + // calls that function before it updates the visible model or some-such). // + ///////////////////////////////////////////////////////////////////////////////////////////////////////////// + const newVisibleJoinTables = getVisibleJoinTables(); + if (JSON.stringify([...newVisibleJoinTables.keys()]) != JSON.stringify([...visibleJoinTables.keys()])) + { + updateTable(); + setVisibleJoinTables(newVisibleJoinTables); + } + const handleColumnOrderChange = (columnOrderChangeParams: GridColumnOrderChangeParams) => { // TODO: make local storaged @@ -689,7 +886,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element // construct the url for the export // ////////////////////////////////////// const d = new Date(); - const dateString = `${d.getFullYear()}-${zp(d.getMonth()+1)}-${zp(d.getDate())} ${zp(d.getHours())}${zp(d.getMinutes())}`; + const dateString = `${d.getFullYear()}-${zp(d.getMonth() + 1)}-${zp(d.getDate())} ${zp(d.getHours())}${zp(d.getMinutes())}`; const filename = `${tableMetaData.label} Export ${dateString}.${format}`; const url = `/data/${tableMetaData.name}/export/${filename}?filter=${encodeURIComponent(JSON.stringify(buildQFilter(tableMetaData, filterModel)))}&fields=${visibleFields.join(",")}`; @@ -748,9 +945,11 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element { if (selectFullFilterState === "filter") { + // todo - distinct? return (totalRecords); } + // todo - distinct? return (selectedIds.length); } @@ -866,13 +1065,25 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element // @ts-ignore const defaultLabelDisplayedRows = ({from, to, count}) => { - if(tableMetaData && !tableMetaData.capabilities.has(Capability.TABLE_COUNT)) + const tooltipHTML = <> + The number of rows shown on this screen may be greater than the number of {tableMetaData?.label} records + that match your query, because you have included fields from other tables which may have + more than one record associated with each {tableMetaData?.label}. + + let distinctPart = isJoinMany(tableMetaData, getVisibleJoinTables()) ? ( +  ({distinctRecords} distinct + info_outlined + + ) + ) : <>; + + if (tableMetaData && !tableMetaData.capabilities.has(Capability.TABLE_COUNT)) { //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // to avoid a non-countable table showing (this is what data-grid did) 91-100 even if there were only 95 records, // // we'll do this... not quite good enough, but better than the original // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - if(rows.length > 0 && rows.length < to - from) + if (rows.length > 0 && rows.length < to - from) { to = from + rows.length; } @@ -886,13 +1097,21 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element { if (count === 0) { - return (loading ? "Counting records..." : "No rows"); + return (loading ? "Counting..." : "No rows"); } - return (`Showing ${from.toLocaleString()} to ${to.toLocaleString()} of ${count !== -1 ? `${count.toLocaleString()} records` : `more than ${to.toLocaleString()} records`}`); + + return <> + Showing {from.toLocaleString()} to {to.toLocaleString()} of + { + count == -1 ? + <>more than {to.toLocaleString()} + : <> {count.toLocaleString()}{distinctPart} + } + ; } else { - return ("Counting records..."); + return ("Counting..."); } }; @@ -903,9 +1122,10 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element component="div" // note - passing null here makes the 'to' param in the defaultLabelDisplayedRows also be null, // so pass some sentinel value... + // todo - distinct? count={totalRecords === null || totalRecords === undefined ? -1 : totalRecords} page={pageNumber} - rowsPerPageOptions={[ 10, 25, 50, 100, 250 ]} + rowsPerPageOptions={[10, 25, 50, 100, 250]} rowsPerPage={rowsPerPage} onPageChange={(event, value) => handlePageChange(value)} onRowsPerPageChange={(event) => handleRowsPerPageChange(Number(event.target.value))} @@ -923,7 +1143,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element async function handleSavedFilterChange(selectedSavedFilterId: number) { - if(selectedSavedFilterId != null) + if (selectedSavedFilterId != null) { const qRecord = await fetchSavedFilter(selectedSavedFilterId); const models = await FilterUtils.determineFilterAndSortModels(qController, tableMetaData, qRecord.values.get("filterJson"), null, null, null); @@ -939,7 +1159,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element } } - async function fetchSavedFilter(filterId: number):Promise + async function fetchSavedFilter(filterId: number): Promise { let qRecord = null; const formData = new FormData(); @@ -966,30 +1186,30 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element qRecord = new QRecord(result.values.savedFilterList[0]); } - return(qRecord); + return (qRecord); } const copyColumnValues = async (column: GridColDef) => { let data = ""; let counter = 0; - if(latestQueryResults && latestQueryResults.length) + if (latestQueryResults && latestQueryResults.length) { let qFieldMetaData = tableMetaData.fields.get(column.field); - for(let i = 0; i < latestQueryResults.length; i++) + for (let i = 0; i < latestQueryResults.length; i++) { let record = latestQueryResults[i] as QRecord; const value = ValueUtils.getUnadornedValueForDisplay(qFieldMetaData, record.values.get(qFieldMetaData.name), record.displayValues.get(qFieldMetaData.name)); - if(value !== null && value !== undefined && String(value) !== "") + if (value !== null && value !== undefined && String(value) !== "") { data += value + "\n"; counter++; } } - if(counter > 0) + if (counter > 0) { - await navigator.clipboard.writeText(data) + await navigator.clipboard.writeText(data); setSuccessAlert(`Copied ${counter} ${qFieldMetaData.label} value${counter == 1 ? "" : "s"}.`); } else @@ -998,13 +1218,13 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element } setTimeout(() => setSuccessAlert(null), 3000); } - } + }; const openColumnStatistics = async (column: GridColDef) => { setFilterForColumnStats(buildQFilter(tableMetaData, filterModel)); setColumnStatsFieldName(column.field); - } + }; const CustomColumnMenu = forwardRef( function GridColumnMenu(props: GridColumnMenuProps, ref) @@ -1029,13 +1249,13 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element - + { hideMenu(e); - copyColumnValues(currentColumn) + copyColumnValues(currentColumn); }}> Copy values @@ -1067,7 +1287,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element const columns = useGridSelector(apiRef, gridColumnDefinitionsSelector); const columnVisibilityModel = useGridSelector(apiRef, gridColumnVisibilityModelSelector); - const [openGroups, setOpenGroups] = useState({} as {[name: string]: boolean}); + const [openGroups, setOpenGroups] = useState({} as { [name: string]: boolean }); const groups = ["Order", "Line Item"]; @@ -1082,21 +1302,21 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element setColumnVisibilityModel(JSON.parse(JSON.stringify(columnVisibilityModel))) */ - console.log(`${fieldName} = ${columnVisibilityModel[fieldName]}`) + console.log(`${fieldName} = ${columnVisibilityModel[fieldName]}`); // columnVisibilityModel[fieldName] = Math.random() < 0.5; apiRef.current.setColumnVisibility(fieldName, columnVisibilityModel[fieldName] === false); // handleColumnVisibilityChange(JSON.parse(JSON.stringify(columnVisibilityModel))); - } + }; const toggleColumnGroup = (groupName: string) => { - if(openGroups[groupName] === undefined) + if (openGroups[groupName] === undefined) { openGroups[groupName] = true; } openGroups[groupName] = !openGroups[groupName]; setOpenGroups(JSON.parse(JSON.stringify(openGroups))); - } + }; console.log("re-render"); return ( @@ -1146,7 +1366,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element ); } - ) + ); function CustomToolbar() { @@ -1235,6 +1455,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element records on this page are selected. @@ -1245,6 +1466,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element selectFullFilterState === "filter" && (
All + {/* todo - distinct? */} {` ${totalRecords ? totalRecords.toLocaleString() : ""} `} records matching this query are selected.