mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-17 21:00:45 +00:00
Joins on Record Query; Count action w/ distinct input/output; JSX Element option for pageHeader
This commit is contained in:
@ -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",
|
||||
|
@ -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()
|
||||
<QContext.Provider value={{
|
||||
pageHeader: pageHeader,
|
||||
accentColor: accentColor,
|
||||
setPageHeader: (header: string) => setPageHeader(header),
|
||||
setPageHeader: (header: string | JSX.Element) => setPageHeader(header),
|
||||
setAccentColor: (accentColor: string) => setAccentColor(accentColor)
|
||||
}}>
|
||||
<ThemeProvider theme={theme}>
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -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 //
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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;
|
||||
|
@ -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";
|
||||
@ -93,7 +95,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
||||
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<string>());
|
||||
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,8 +181,8 @@ 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});
|
||||
|
||||
@ -304,7 +311,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
||||
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([]);
|
||||
@ -325,6 +332,89 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
||||
return (filter);
|
||||
};
|
||||
|
||||
const getVisibleJoinTables = (): Set<string> =>
|
||||
{
|
||||
const visibleJoinTables = new Set<string>();
|
||||
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<string>): 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>): 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 = <div>
|
||||
You are viewing results from the {tableMetaData.label} table joined with the {joinLabelsString} table{joinLabels.length == 1 ? "" : "s"}
|
||||
</div>
|
||||
|
||||
return(
|
||||
<div>
|
||||
{tableMetaData?.label}
|
||||
<CustomWidthTooltip title={tooltipHTML}>
|
||||
<IconButton sx={{p: 0, fontSize: "0.5rem", mb: 1, color: "#9f9f9f", fontVariationSettings: "'wght' 100"}}><Icon fontSize="small">emergency</Icon></IconButton>
|
||||
</CustomWidthTooltip>
|
||||
</div>);
|
||||
}
|
||||
else
|
||||
{
|
||||
return (tableMetaData?.label);
|
||||
}
|
||||
};
|
||||
|
||||
const updateTable = () =>
|
||||
{
|
||||
setLoading(true);
|
||||
@ -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), //
|
||||
@ -355,12 +467,35 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
||||
|
||||
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,8 +615,16 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
||||
console.log(`No count results for id ${latestQueryId}...`);
|
||||
return;
|
||||
}
|
||||
setTotalRecords(countResults[latestQueryId]);
|
||||
try
|
||||
{
|
||||
setTotalRecords(countResults[latestQueryId][0]);
|
||||
setDistinctRecords(countResults[latestQueryId][1]);
|
||||
delete countResults[latestQueryId];
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
console.log(e);
|
||||
}
|
||||
}, [receivedCountTimestamp]);
|
||||
|
||||
///////////////////////////
|
||||
@ -462,8 +647,7 @@ 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);
|
||||
@ -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
|
||||
@ -748,9 +945,11 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
||||
{
|
||||
if (selectFullFilterState === "filter")
|
||||
{
|
||||
// todo - distinct?
|
||||
return (totalRecords);
|
||||
}
|
||||
|
||||
// todo - distinct?
|
||||
return (selectedIds.length);
|
||||
}
|
||||
|
||||
@ -866,6 +1065,18 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
||||
// @ts-ignore
|
||||
const defaultLabelDisplayedRows = ({from, to, 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()) ? (<Box display="inline" textAlign="right">
|
||||
({distinctRecords} distinct<CustomWidthTooltip title={tooltipHTML}>
|
||||
<IconButton sx={{p: 0, pl: 0.0, mb: 0.25}}><Icon fontSize="small" sx={{fontSize: "1.125rem !important", color: "#9f9f9f"}}>info_outlined</Icon></IconButton>
|
||||
</CustomWidthTooltip>
|
||||
)
|
||||
</Box>) : <></>;
|
||||
|
||||
if (tableMetaData && !tableMetaData.capabilities.has(Capability.TABLE_COUNT))
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -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,6 +1122,7 @@ 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]}
|
||||
@ -989,7 +1209,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
||||
|
||||
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<HTMLUListElement, GridColumnMenuProps>(
|
||||
function GridColumnMenu(props: GridColumnMenuProps, ref)
|
||||
@ -1035,7 +1255,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
||||
<MenuItem sx={{justifyContent: "space-between"}} onClick={(e) =>
|
||||
{
|
||||
hideMenu(e);
|
||||
copyColumnValues(currentColumn)
|
||||
copyColumnValues(currentColumn);
|
||||
}}>
|
||||
Copy values
|
||||
|
||||
@ -1082,11 +1302,11 @@ 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) =>
|
||||
{
|
||||
@ -1096,7 +1316,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
||||
}
|
||||
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
|
||||
</div>
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
function CustomToolbar()
|
||||
{
|
||||
@ -1235,6 +1455,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
||||
records on this page are selected.
|
||||
<Button onClick={() => setSelectFullFilterState("filter")}>
|
||||
Select all
|
||||
{/*todo - distinct?*/}
|
||||
{` ${totalRecords ? totalRecords.toLocaleString() : ""} `}
|
||||
records matching this query
|
||||
</Button>
|
||||
@ -1245,6 +1466,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
||||
selectFullFilterState === "filter" && (
|
||||
<div className="selectionTool">
|
||||
All
|
||||
{/* todo - distinct? */}
|
||||
<strong>{` ${totalRecords ? totalRecords.toLocaleString() : ""} `}</strong>
|
||||
records matching this query are selected.
|
||||
<Button onClick={() => setSelectFullFilterState("checked")}>
|
||||
@ -1269,20 +1491,20 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
||||
{
|
||||
menuItems.push(<Divider />);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const menuItems: JSX.Element[] = [];
|
||||
if (table.capabilities.has(Capability.TABLE_INSERT) && table.insertPermission)
|
||||
{
|
||||
menuItems.push(<MenuItem onClick={bulkLoadClicked}><ListItemIcon><Icon>library_add</Icon></ListItemIcon>Bulk Load</MenuItem>)
|
||||
menuItems.push(<MenuItem onClick={bulkLoadClicked}><ListItemIcon><Icon>library_add</Icon></ListItemIcon>Bulk Load</MenuItem>);
|
||||
}
|
||||
if (table.capabilities.has(Capability.TABLE_UPDATE) && table.editPermission)
|
||||
{
|
||||
menuItems.push(<MenuItem onClick={bulkEditClicked}><ListItemIcon><Icon>edit</Icon></ListItemIcon>Bulk Edit</MenuItem>)
|
||||
menuItems.push(<MenuItem onClick={bulkEditClicked}><ListItemIcon><Icon>edit</Icon></ListItemIcon>Bulk Edit</MenuItem>);
|
||||
}
|
||||
if (table.capabilities.has(Capability.TABLE_DELETE) && table.deletePermission)
|
||||
{
|
||||
menuItems.push(<MenuItem onClick={bulkDeleteClicked}><ListItemIcon><Icon>delete</Icon></ListItemIcon>Bulk Delete</MenuItem>)
|
||||
menuItems.push(<MenuItem onClick={bulkDeleteClicked}><ListItemIcon><Icon>delete</Icon></ListItemIcon>Bulk Delete</MenuItem>);
|
||||
}
|
||||
|
||||
const runRecordScriptProcess = metaData?.processes.get("runRecordScript");
|
||||
@ -1307,7 +1529,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
||||
|
||||
if (menuItems.length === 0)
|
||||
{
|
||||
menuItems.push(<MenuItem disabled><ListItemIcon><Icon>block</Icon></ListItemIcon><i>No actions available</i></MenuItem>)
|
||||
menuItems.push(<MenuItem disabled><ListItemIcon><Icon>block</Icon></ListItemIcon><i>No actions available</i></MenuItem>);
|
||||
}
|
||||
|
||||
const renderActionsMenu = (
|
||||
@ -1351,6 +1573,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
||||
useEffect(() =>
|
||||
{
|
||||
setTotalRecords(null);
|
||||
setDistinctRecords(null);
|
||||
updateTable();
|
||||
}, [tableState, filterModel]);
|
||||
|
||||
@ -1438,7 +1661,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
||||
<Card>
|
||||
<Box height="100%">
|
||||
<DataGridPro
|
||||
components={{Toolbar: CustomToolbar, Pagination: CustomPagination, LoadingOverlay: Loading, ColumnMenu: CustomColumnMenu, ColumnsPanel: CustomColumnsPanel}}
|
||||
components={{Toolbar: CustomToolbar, Pagination: CustomPagination, LoadingOverlay: Loading, ColumnMenu: CustomColumnMenu/*, ColumnsPanel: CustomColumnsPanel*/}}
|
||||
pinnedColumns={pinnedColumns}
|
||||
onPinnedColumnsChange={handlePinnedColumnsChange}
|
||||
pagination
|
||||
@ -1453,7 +1676,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
||||
// getRowHeight={() => "auto"} // maybe nice? wraps values in cells...
|
||||
columns={columnsModel}
|
||||
rowBuffer={10}
|
||||
rowCount={totalRecords === null || totalRecords === undefined ? 0 : totalRecords}
|
||||
rowCount={/*todo - distinct?*/totalRecords === null || totalRecords === undefined ? 0 : totalRecords}
|
||||
onPageSizeChange={handleRowsPerPageChange}
|
||||
onRowClick={handleRowClick}
|
||||
onStateChange={handleStateChange}
|
||||
@ -1469,6 +1692,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
||||
sortingOrder={["asc", "desc"]}
|
||||
sortModel={columnSortModel}
|
||||
getRowClassName={(params) => (params.indexRelativeToCurrentPage % 2 === 0 ? "even" : "odd")}
|
||||
getRowId={(row) => row.__qRowIndex}
|
||||
/>
|
||||
</Box>
|
||||
</Card>
|
||||
|
@ -36,24 +36,35 @@ export default class DataGridUtils
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static makeRows = (results: QRecord[], tableMetaData: QTableMetaData): {rows: GridRowsProp[], columnsToRender: any} =>
|
||||
public static makeRows = (results: QRecord[], tableMetaData: QTableMetaData): GridRowsProp[] =>
|
||||
{
|
||||
const fields = [ ...tableMetaData.fields.values() ];
|
||||
const rows = [] as any[];
|
||||
const columnsToRender = {} as any;
|
||||
results.forEach((record: QRecord) =>
|
||||
{
|
||||
const row: any = {};
|
||||
row.__qRowIndex = record.values.get("__qRowIndex");
|
||||
|
||||
fields.forEach((field) =>
|
||||
{
|
||||
const value = ValueUtils.getDisplayValue(field, record, "query");
|
||||
if (typeof value !== "string")
|
||||
{
|
||||
columnsToRender[field.name] = true;
|
||||
}
|
||||
row[field.name] = value;
|
||||
row[field.name] = ValueUtils.getDisplayValue(field, record, "query");
|
||||
});
|
||||
|
||||
if(tableMetaData.exposedJoins)
|
||||
{
|
||||
for (let i = 0; i < tableMetaData.exposedJoins.length; i++)
|
||||
{
|
||||
const join = tableMetaData.exposedJoins[i];
|
||||
|
||||
const fields = [ ...join.joinTable.fields.values() ];
|
||||
fields.forEach((field) =>
|
||||
{
|
||||
let fieldName = join.joinTable.name + "." + field.name;
|
||||
row[fieldName] = ValueUtils.getDisplayValue(field, record, "query", fieldName);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if(!row["id"])
|
||||
{
|
||||
row["id"] = record.values.get(tableMetaData.primaryKeyField) ?? row[tableMetaData.primaryKeyField];
|
||||
@ -69,29 +80,37 @@ export default class DataGridUtils
|
||||
rows.push(row);
|
||||
});
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// do this secondary check for columnsToRender - in case we didn't have any rows above, and our check for string isn't enough. //
|
||||
// ... shouldn't this be just based on the field definition anyway... ? plus adornments? //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
fields.forEach((field) =>
|
||||
{
|
||||
if(field.possibleValueSourceName)
|
||||
{
|
||||
columnsToRender[field.name] = true;
|
||||
}
|
||||
});
|
||||
|
||||
return ({rows, columnsToRender});
|
||||
return (rows);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static setupGridColumns = (tableMetaData: QTableMetaData, columnsToRender: any, linkBase: string = ""): GridColDef[] =>
|
||||
public static setupGridColumns = (tableMetaData: QTableMetaData, linkBase: string = ""): GridColDef[] =>
|
||||
{
|
||||
const columns = [] as GridColDef[];
|
||||
const sortedKeys: string[] = [];
|
||||
this.addColumnsForTable(tableMetaData, linkBase, columns, null);
|
||||
|
||||
if(tableMetaData.exposedJoins)
|
||||
{
|
||||
for (let i = 0; i < tableMetaData.exposedJoins.length; i++)
|
||||
{
|
||||
const join = tableMetaData.exposedJoins[i];
|
||||
// todo - link base here - link to the join table
|
||||
this.addColumnsForTable(join.joinTable, null, columns, join.joinTable.name + ".", join.label + ": ");
|
||||
}
|
||||
}
|
||||
|
||||
return (columns);
|
||||
};
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static addColumnsForTable(tableMetaData: QTableMetaData, linkBase: string, columns: GridColDef[], namePrefix?: string, labelPrefix?: string)
|
||||
{
|
||||
const sortedKeys: string[] = [];
|
||||
for (let i = 0; i < tableMetaData.sections.length; i++)
|
||||
{
|
||||
const section = tableMetaData.sections[i];
|
||||
@ -109,7 +128,7 @@ export default class DataGridUtils
|
||||
sortedKeys.forEach((key) =>
|
||||
{
|
||||
const field = tableMetaData.fields.get(key);
|
||||
const column = this.makeColumnFromField(field, tableMetaData, columnsToRender);
|
||||
const column = this.makeColumnFromField(field, tableMetaData, namePrefix, labelPrefix);
|
||||
|
||||
if (key === tableMetaData.primaryKeyField && linkBase)
|
||||
{
|
||||
@ -123,16 +142,12 @@ export default class DataGridUtils
|
||||
columns.push(column);
|
||||
}
|
||||
});
|
||||
|
||||
return (columns);
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static makeColumnFromField = (field: QFieldMetaData, tableMetaData: QTableMetaData, columnsToRender: any): GridColDef =>
|
||||
public static makeColumnFromField = (field: QFieldMetaData, tableMetaData: QTableMetaData, namePrefix?: string, labelPrefix?: string): GridColDef =>
|
||||
{
|
||||
let columnType = "string";
|
||||
let columnWidth = 200;
|
||||
@ -198,24 +213,21 @@ export default class DataGridUtils
|
||||
}
|
||||
}
|
||||
|
||||
let headerName = labelPrefix ? labelPrefix + field.label : field.label;
|
||||
let fieldName = namePrefix ? namePrefix + field.name : field.name;
|
||||
|
||||
const column = {
|
||||
field: field.name,
|
||||
field: fieldName,
|
||||
type: columnType,
|
||||
headerName: field.label,
|
||||
headerName: headerName,
|
||||
width: columnWidth,
|
||||
renderCell: null as any,
|
||||
filterOperators: filterOperators,
|
||||
};
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
// looks like, maybe we can just always render all columns, and remove this parameter? //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
if (columnsToRender == null || columnsToRender[field.name])
|
||||
{
|
||||
column.renderCell = (cellValues: any) => (
|
||||
(cellValues.value)
|
||||
);
|
||||
}
|
||||
|
||||
return (column);
|
||||
}
|
||||
|
@ -69,10 +69,12 @@ class ValueUtils
|
||||
** When you have a field, and a record - call this method to get a string or
|
||||
** element back to display the field's value.
|
||||
*******************************************************************************/
|
||||
public static getDisplayValue(field: QFieldMetaData, record: QRecord, usage: "view" | "query" = "view"): string | JSX.Element | JSX.Element[]
|
||||
public static getDisplayValue(field: QFieldMetaData, record: QRecord, usage: "view" | "query" = "view", overrideFieldName?: string): string | JSX.Element | JSX.Element[]
|
||||
{
|
||||
const displayValue = record.displayValues ? record.displayValues.get(field.name) : undefined;
|
||||
const rawValue = record.values ? record.values.get(field.name) : undefined;
|
||||
const fieldName = overrideFieldName ?? field.name;
|
||||
|
||||
const displayValue = record.displayValues ? record.displayValues.get(fieldName) : undefined;
|
||||
const rawValue = record.values ? record.values.get(fieldName) : undefined;
|
||||
|
||||
return ValueUtils.getValueForDisplay(field, rawValue, displayValue, usage);
|
||||
}
|
||||
|
Reference in New Issue
Block a user