Checkpoint of joins, new handling of the select-all/page/ and new 'subset' options

This commit is contained in:
2023-04-26 11:44:13 -05:00
parent 19697e7360
commit e99cfcb7ff
4 changed files with 282 additions and 61 deletions

View File

@ -50,18 +50,20 @@ export function QCreateNewButton({tablePath}: QCreateNewButtonProps): JSX.Elemen
interface QSaveButtonProps interface QSaveButtonProps
{ {
label?: string; label?: string;
iconName?: string;
onClickHandler?: any, onClickHandler?: any,
disabled: boolean disabled: boolean
} }
QSaveButton.defaultProps = { QSaveButton.defaultProps = {
label: "Save" label: "Save",
iconName: "save"
}; };
export function QSaveButton({label, onClickHandler, disabled}: QSaveButtonProps): JSX.Element export function QSaveButton({label, iconName, onClickHandler, disabled}: QSaveButtonProps): JSX.Element
{ {
return ( return (
<Box ml={3} width={standardWidth}> <Box ml={3} width={standardWidth}>
<MDButton type="submit" variant="gradient" color="info" size="small" onClick={onClickHandler} fullWidth startIcon={<Icon>save</Icon>} disabled={disabled}> <MDButton type="submit" variant="gradient" color="info" size="small" onClick={onClickHandler} fullWidth startIcon={<Icon>{iconName}</Icon>} disabled={disabled}>
{label} {label}
</MDButton> </MDButton>
</Box> </Box>

View File

@ -48,15 +48,16 @@ import Modal from "@mui/material/Modal";
import Stack from "@mui/material/Stack"; import Stack from "@mui/material/Stack";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import Tooltip from "@mui/material/Tooltip"; import Tooltip from "@mui/material/Tooltip";
import Typography from "@mui/material/Typography"; import {DataGridPro, GridCallbackDetails, GridColDef, GridColumnMenuContainer, GridColumnMenuProps, GridColumnOrderChangeParams, GridColumnPinningMenuItems, GridColumnsMenuItem, GridColumnVisibilityModel, GridDensity, GridEventListener, GridExportMenuItemProps, GridFilterMenuItem, GridFilterModel, GridPinnedColumns, gridPreferencePanelStateSelector, GridRowId, GridRowParams, GridRowProps, 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 {GridColumnsPanelProps} from "@mui/x-data-grid/components/panel/GridColumnsPanel";
import {gridColumnDefinitionsSelector, gridColumnVisibilityModelSelector} from "@mui/x-data-grid/hooks/features/columns/gridColumnsSelector"; import {gridColumnDefinitionsSelector, gridColumnVisibilityModelSelector} from "@mui/x-data-grid/hooks/features/columns/gridColumnsSelector";
import {GridRowModel} from "@mui/x-data-grid/models/gridRows";
import FormData from "form-data"; import FormData from "form-data";
import React, {forwardRef, useContext, useEffect, useReducer, useRef, useState} from "react"; import React, {forwardRef, useContext, useEffect, useReducer, useRef, useState} from "react";
import {useLocation, useNavigate, useSearchParams} from "react-router-dom"; import {useLocation, useNavigate, useSearchParams} from "react-router-dom";
import QContext from "QContext"; import QContext from "QContext";
import {QActionsMenuButton, QCancelButton, QCreateNewButton} from "qqq/components/buttons/DefaultButtons"; import {QActionsMenuButton, QCancelButton, QCreateNewButton, QSaveButton} from "qqq/components/buttons/DefaultButtons";
import MenuButton from "qqq/components/buttons/MenuButton";
import SavedFilters from "qqq/components/misc/SavedFilters"; import SavedFilters from "qqq/components/misc/SavedFilters";
import CustomWidthTooltip from "qqq/components/tooltips/CustomWidthTooltip"; import CustomWidthTooltip from "qqq/components/tooltips/CustomWidthTooltip";
import BaseLayout from "qqq/layouts/BaseLayout"; import BaseLayout from "qqq/layouts/BaseLayout";
@ -165,7 +166,11 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
const [totalRecords, setTotalRecords] = useState(null); const [totalRecords, setTotalRecords] = useState(null);
const [distinctRecords, setDistinctRecords] = useState(null); const [distinctRecords, setDistinctRecords] = useState(null);
const [selectedIds, setSelectedIds] = useState([] as string[]); const [selectedIds, setSelectedIds] = useState([] as string[]);
const [selectFullFilterState, setSelectFullFilterState] = useState("n/a" as "n/a" | "checked" | "filter"); const [distinctRecordsOnPageCount, setDistinctRecordsOnPageCount] = useState(null as number);
const [selectionSubsetSize, setSelectionSubsetSize] = useState(null as number);
const [selectionSubsetSizePromptOpen, setSelectionSubsetSizePromptOpen] = useState(false);
const [selectFullFilterState, setSelectFullFilterState] = useState("n/a" as "n/a" | "checked" | "filter" | "filterSubset");
const [rowSelectionModel, setRowSelectionModel] = useState<GridSelectionModel>([]);
const [columnsModel, setColumnsModel] = useState([] as GridColDef[]); const [columnsModel, setColumnsModel] = useState([] as GridColDef[]);
const [rows, setRows] = useState([] as GridRowsProp[]); const [rows, setRows] = useState([] as GridRowsProp[]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@ -325,9 +330,9 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
// first time we call in here, we may not yet have set it in state (but will have fetched it async) // // first time we call in here, we may not yet have set it in state (but will have fetched it async) //
// so we'll pass in the local version of it! // // so we'll pass in the local version of it! //
////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////
const buildQFilter = (tableMetaData: QTableMetaData, filterModel: GridFilterModel) => const buildQFilter = (tableMetaData: QTableMetaData, filterModel: GridFilterModel, limit?: number) =>
{ {
const filter = FilterUtils.buildQFilterFromGridFilter(tableMetaData, filterModel, columnSortModel); const filter = FilterUtils.buildQFilterFromGridFilter(tableMetaData, filterModel, columnSortModel, limit);
setHasValidFilters(filter.criteria && filter.criteria.length > 0); setHasValidFilters(filter.criteria && filter.criteria.length > 0);
return (filter); return (filter);
}; };
@ -469,7 +474,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
{ {
let linkBase = metaData.getTablePath(table); let linkBase = metaData.getTablePath(table);
linkBase += linkBase.endsWith("/") ? "" : "/"; linkBase += linkBase.endsWith("/") ? "" : "/";
const columns = DataGridUtils.setupGridColumns(tableMetaData, linkBase); const columns = DataGridUtils.setupGridColumns(tableMetaData, linkBase, metaData);
setColumnsModel(columns); setColumnsModel(columns);
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@ -520,6 +525,8 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
} }
const qFilter = buildQFilter(tableMetaData, localFilterModel); const qFilter = buildQFilter(tableMetaData, localFilterModel);
qFilter.skip = pageNumber * rowsPerPage;
qFilter.limit = rowsPerPage;
////////////////////////////////////////// //////////////////////////////////////////
// figure out joins to use in the query // // figure out joins to use in the query //
@ -562,7 +569,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
}); });
} }
qController.query(tableName, qFilter, rowsPerPage, pageNumber * rowsPerPage, queryJoins).then((results) => qController.query(tableName, qFilter, queryJoins).then((results) =>
{ {
console.log(`Received results for query ${thisQueryId}`); console.log(`Received results for query ${thisQueryId}`);
queryResults[thisQueryId] = results; queryResults[thisQueryId] = results;
@ -642,6 +649,19 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
delete queryResults[latestQueryId]; delete queryResults[latestQueryId];
setLatestQueryResults(results); setLatestQueryResults(results);
///////////////////////////////////////////////////////////
// count how many distinct primary keys are on this page //
///////////////////////////////////////////////////////////
let distinctPrimaryKeySet = new Set<string>();
for(let i = 0; i < results.length; i++)
{
distinctPrimaryKeySet.add(results[i].values.get(tableMetaData.primaryKeyField) as string);
}
setDistinctRecordsOnPageCount(distinctPrimaryKeySet.size);
////////////////////////////////
// make the rows for the grid //
////////////////////////////////
const rows = DataGridUtils.makeRows(results, tableMetaData); const rows = DataGridUtils.makeRows(results, tableMetaData);
setRows(rows); setRows(rows);
@ -748,19 +768,22 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
const selectionChanged = (selectionModel: GridSelectionModel, details: GridCallbackDetails) => const selectionChanged = (selectionModel: GridSelectionModel, details: GridCallbackDetails) =>
{ {
const newSelectedIds: string[] = []; ////////////////////////////////////////////////////
// since we manage this object, we must re-set it //
////////////////////////////////////////////////////
setRowSelectionModel(selectionModel);
let checkboxesChecked = 0;
let selectedPrimaryKeys = new Set<string>();
selectionModel.forEach((value: GridRowId, index: number) => selectionModel.forEach((value: GridRowId, index: number) =>
{ {
let valueToPush = value as string; checkboxesChecked++
if (tableMetaData.primaryKeyField !== "id") const valueToPush = latestQueryResults[value as number].values.get(tableMetaData.primaryKeyField);
{ selectedPrimaryKeys.add(valueToPush as string);
valueToPush = latestQueryResults[index].values.get(tableMetaData.primaryKeyField);
}
newSelectedIds.push(valueToPush as string);
}); });
setSelectedIds(newSelectedIds); setSelectedIds([...selectedPrimaryKeys.values()]);
if (newSelectedIds.length === rowsPerPage) if (checkboxesChecked === rowsPerPage)
{ {
setSelectFullFilterState("checked"); setSelectFullFilterState("checked");
} }
@ -940,11 +963,13 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
{ {
if (selectFullFilterState === "filter") if (selectFullFilterState === "filter")
{ {
// todo - distinct? if(isJoinMany(tableMetaData, getVisibleJoinTables()))
{
return (distinctRecords);
}
return (totalRecords); return (totalRecords);
} }
// todo - distinct?
return (selectedIds.length); return (selectedIds.length);
} }
@ -955,6 +980,11 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
return `?recordsParam=filterJSON&filterJSON=${JSON.stringify(buildQFilter(tableMetaData, filterModel))}`; return `?recordsParam=filterJSON&filterJSON=${JSON.stringify(buildQFilter(tableMetaData, filterModel))}`;
} }
if (selectFullFilterState === "filterSubset")
{
return `?recordsParam=filterJSON&filterJSON=${JSON.stringify(buildQFilter(tableMetaData, filterModel, selectionSubsetSize))}`;
}
if (selectedIds.length > 0) if (selectedIds.length > 0)
{ {
return `?recordsParam=recordIds&recordIds=${selectedIds.join(",")}`; return `?recordsParam=recordIds&recordIds=${selectedIds.join(",")}`;
@ -969,6 +999,10 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
{ {
setRecordIdsForProcess(buildQFilter(tableMetaData, filterModel)); setRecordIdsForProcess(buildQFilter(tableMetaData, filterModel));
} }
else if (selectFullFilterState === "filterSubset")
{
setRecordIdsForProcess(buildQFilter(tableMetaData, filterModel, selectionSubsetSize));
}
else if (selectedIds.length > 0) else if (selectedIds.length > 0)
{ {
setRecordIdsForProcess(selectedIds.join(",")); setRecordIdsForProcess(selectedIds.join(","));
@ -1067,7 +1101,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
</> </>
let distinctPart = isJoinMany(tableMetaData, getVisibleJoinTables()) ? (<Box display="inline" textAlign="right"> let distinctPart = isJoinMany(tableMetaData, getVisibleJoinTables()) ? (<Box display="inline" textAlign="right">
&nbsp;({distinctRecords} distinct<CustomWidthTooltip title={tooltipHTML}> &nbsp;({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> <IconButton sx={{p: 0, pl: 0.25, mb: 0.25}}><Icon fontSize="small" sx={{fontSize: "1.125rem !important", color: "#9f9f9f"}}>info_outlined</Icon></IconButton>
</CustomWidthTooltip> </CustomWidthTooltip>
) )
</Box>) : <></>; </Box>) : <></>;
@ -1116,8 +1150,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
<TablePagination <TablePagination
component="div" component="div"
// note - passing null here makes the 'to' param in the defaultLabelDisplayedRows also be null, // note - passing null here makes the 'to' param in the defaultLabelDisplayedRows also be null,
// so pass some sentinel value... // so pass a sentinel value of -1...
// todo - distinct?
count={totalRecords === null || totalRecords === undefined ? -1 : totalRecords} count={totalRecords === null || totalRecords === undefined ? -1 : totalRecords}
page={pageNumber} page={pageNumber}
rowsPerPageOptions={[10, 25, 50, 100, 250]} rowsPerPageOptions={[10, 25, 50, 100, 250]}
@ -1275,6 +1308,9 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
); );
}); });
////////////////////////////////////////////////////////////////////////////
// this is a WIP example of how we could do a custom "columns" panel/menu //
////////////////////////////////////////////////////////////////////////////
const CustomColumnsPanel = forwardRef<any, GridColumnsPanelProps>( const CustomColumnsPanel = forwardRef<any, GridColumnsPanelProps>(
function MyCustomColumnsPanel(props: GridColumnsPanelProps, ref) function MyCustomColumnsPanel(props: GridColumnsPanelProps, ref)
{ {
@ -1313,7 +1349,6 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
setOpenGroups(JSON.parse(JSON.stringify(openGroups))); setOpenGroups(JSON.parse(JSON.stringify(openGroups)));
}; };
console.log("re-render");
return ( return (
<div ref={ref} className="custom-columns-panel" style={{width: "350px", height: "450px"}}> <div ref={ref} className="custom-columns-panel" style={{width: "350px", height: "450px"}}>
<Box height="55px" padding="5px"> <Box height="55px" padding="5px">
@ -1395,14 +1430,84 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
setGridPreferencesWindow(preferencePanelState.openedPanelValue); setGridPreferencesWindow(preferencePanelState.openedPanelValue);
}); });
const joinIsMany = isJoinMany(tableMetaData, visibleJoinTables);
const safeToLocaleString = (n: Number): string =>
{
if(n != null && n != undefined)
{
return (n.toLocaleString());
}
return ("");
}
const selectionMenuOptions: string[] = [];
selectionMenuOptions.push(`This page (${safeToLocaleString(distinctRecordsOnPageCount)} ${joinIsMany ? "distinct " : ""}record${distinctRecordsOnPageCount == 1 ? "" : "s"})`);
selectionMenuOptions.push(`Full query result (${joinIsMany ? safeToLocaleString(distinctRecords) + ` distinct record${distinctRecords == 1 ? "" : "s"}` : safeToLocaleString(totalRecords) + ` record${totalRecords == 1 ? "" : "s"}`})`);
selectionMenuOptions.push(`Subset of the query result ${selectionSubsetSize ? `(${safeToLocaleString(selectionSubsetSize)} ${joinIsMany ? "distinct " : ""}record${selectionSubsetSize == 1 ? "" : "s"})` : "..."}`);
selectionMenuOptions.push("Clear selection");
function programmaticallySelectSomeOrAllRows(max?: number)
{
///////////////////////////////////////////////////////////////////////////////////////////
// any time the user selects one of the options from our selection menu, //
// we want to check all the boxes on the screen - and - "select" all of the primary keys //
// unless they did the subset option - then we'll only go up to a 'max' number //
///////////////////////////////////////////////////////////////////////////////////////////
const rowSelectionModel: GridSelectionModel = [];
let selectedPrimaryKeys = new Set<string>();
rows.forEach((value: GridRowModel, index: number) =>
{
const primaryKeyValue = latestQueryResults[index].values.get(tableMetaData.primaryKeyField);
if(max)
{
if(selectedPrimaryKeys.size < max)
{
if(!selectedPrimaryKeys.has(primaryKeyValue))
{
rowSelectionModel.push(value.__rowIndex);
selectedPrimaryKeys.add(primaryKeyValue as string);
}
}
}
else
{
rowSelectionModel.push(value.__rowIndex);
selectedPrimaryKeys.add(primaryKeyValue as string);
}
});
setRowSelectionModel(rowSelectionModel);
setSelectedIds([...selectedPrimaryKeys.values()]);
}
const selectionMenuCallback = (selectedIndex: number) =>
{
if(selectedIndex == 0)
{
programmaticallySelectSomeOrAllRows();
setSelectFullFilterState("checked")
}
else if(selectedIndex == 1)
{
programmaticallySelectSomeOrAllRows();
setSelectFullFilterState("filter")
}
else if(selectedIndex == 2)
{
setSelectionSubsetSizePromptOpen(true);
}
else if(selectedIndex == 3)
{
setSelectFullFilterState("n/a")
setRowSelectionModel([]);
setSelectedIds([]);
}
};
return ( return (
<GridToolbarContainer> <GridToolbarContainer>
<div> <div>
<Button <Button id="refresh-button" onClick={updateTable} startIcon={<Icon>refresh</Icon>} sx={{pr: "1.25rem"}}>
id="refresh-button"
onClick={updateTable}
startIcon={<Icon>refresh</Icon>}
>
Refresh Refresh
</Button> </Button>
</div> </div>
@ -1416,19 +1521,27 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
<Tooltip title="Clear All Filters"> <Tooltip title="Clear All Filters">
<Icon sx={{cursor: "pointer"}} onClick={() => setShowClearFiltersWarning(true)}>clear</Icon> <Icon sx={{cursor: "pointer"}} onClick={() => setShowClearFiltersWarning(true)}>clear</Icon>
</Tooltip> </Tooltip>
<Dialog open={showClearFiltersWarning} onClose={() => setShowClearFiltersWarning(false)}> <Dialog open={showClearFiltersWarning} onClose={() => setShowClearFiltersWarning(false)} onKeyPress={(e) =>
{
if (e.key == "Enter")
{
setShowClearFiltersWarning(false)
navigate(metaData.getTablePathByName(tableName));
handleFilterChange({items: []} as GridFilterModel);
}
}}>
<DialogTitle id="alert-dialog-title">Confirm</DialogTitle> <DialogTitle id="alert-dialog-title">Confirm</DialogTitle>
<DialogContent> <DialogContent>
<DialogContentText>Are you sure you want to clear all filters?</DialogContentText> <DialogContentText>Are you sure you want to clear all filters?</DialogContentText>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={() => setShowClearFiltersWarning(false)}>No</Button> <QCancelButton label="No" disabled={false} onClickHandler={() => setShowClearFiltersWarning(false)} />
<Button onClick={() => <QSaveButton label="Yes" iconName="check" disabled={false} onClickHandler={() =>
{ {
setShowClearFiltersWarning(false); setShowClearFiltersWarning(false);
navigate(metaData.getTablePathByName(tableName)); navigate(metaData.getTablePathByName(tableName));
handleFilterChange({items: []} as GridFilterModel); handleFilterChange({items: []} as GridFilterModel);
}}>Yes</Button> }}/>
</DialogActions> </DialogActions>
</Dialog> </Dialog>
</div> </div>
@ -1441,34 +1554,67 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
<ExportMenuItem format="json" /> <ExportMenuItem format="json" />
</GridToolbarExportContainer> </GridToolbarExportContainer>
</div> </div>
<div>
<MenuButton label="Selection" iconName={selectedIds.length == 0 ? "check_box_outline_blank" : "check_box"} disabled={totalRecords == 0} options={selectionMenuOptions} callback={selectionMenuCallback}/>
<SelectionSubsetDialog isOpen={selectionSubsetSizePromptOpen} initialValue={selectionSubsetSize} closeHandler={(value) =>
{
setSelectionSubsetSizePromptOpen(false);
if(value !== undefined)
{
if(typeof value === "number" && value > 0)
{
programmaticallySelectSomeOrAllRows(value);
setSelectionSubsetSize(value);
setSelectFullFilterState("filterSubset")
}
else
{
setAlertContent("Unexpected value: " + value);
}
}
}} />
</div>
<div> <div>
{ {
selectFullFilterState === "checked" && ( selectFullFilterState === "checked" && (
<div className="selectionTool"> <div className="selectionTool">
The The
<strong>{` ${selectedIds.length.toLocaleString()} `}</strong> <strong>{` ${selectedIds.length.toLocaleString()} `}</strong>
records on this page are selected. {joinIsMany ? " distinct " : ""}
<Button onClick={() => setSelectFullFilterState("filter")}> record{selectedIds.length == 1 ? "" : "s"} on this page {selectedIds.length == 1 ? "is" : "are"} selected.
Select all
{/*todo - distinct?*/}
{` ${totalRecords ? totalRecords.toLocaleString() : ""} `}
records matching this query
</Button>
</div> </div>
) )
} }
{ {
selectFullFilterState === "filter" && ( selectFullFilterState === "filter" && (
<div className="selectionTool"> <div className="selectionTool">
All {
{/* todo - distinct? */} (joinIsMany
<strong>{` ${totalRecords ? totalRecords.toLocaleString() : ""} `}</strong> ? (
records matching this query are selected. distinctRecords == 1
<Button onClick={() => setSelectFullFilterState("checked")}> ? (<>The <strong>only 1</strong> distinct record matching this query is selected.</>)
Select the : (<>All <strong>{(distinctRecords ? distinctRecords.toLocaleString() : "")}</strong> distinct records matching this query are selected.</>)
{` ${selectedIds.length.toLocaleString()} `} )
records on this page : (<>All <strong>{totalRecords ? totalRecords.toLocaleString() : ""}</strong> records matching this query are selected.</>)
</Button> )
}
</div>
)
}
{
selectFullFilterState === "filterSubset" && (
<div className="selectionTool">
The <a onClick={() => setSelectionSubsetSizePromptOpen(true)} style={{cursor: "pointer"}}><strong>first {safeToLocaleString(selectionSubsetSize)}</strong></a> {joinIsMany ? "distinct" : ""} record{selectionSubsetSize == 1 ? "" : "s"} matching this query {selectionSubsetSize == 1 ? "is" : "are"} selected.
</div>
)
}
{
(selectFullFilterState === "n/a" && selectedIds.length > 0) && (
<div className="selectionTool">
<strong>{safeToLocaleString(selectedIds.length)}</strong> {joinIsMany ? "distinct" : ""} {selectedIds.length == 1 ? "record is" : "records are"} selected.
</div> </div>
) )
} }
@ -1671,7 +1817,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
// getRowHeight={() => "auto"} // maybe nice? wraps values in cells... // getRowHeight={() => "auto"} // maybe nice? wraps values in cells...
columns={columnsModel} columns={columnsModel}
rowBuffer={10} rowBuffer={10}
rowCount={/*todo - distinct?*/totalRecords === null || totalRecords === undefined ? 0 : totalRecords} rowCount={totalRecords === null || totalRecords === undefined ? 0 : totalRecords}
onPageSizeChange={handleRowsPerPageChange} onPageSizeChange={handleRowsPerPageChange}
onRowClick={handleRowClick} onRowClick={handleRowClick}
onStateChange={handleStateChange} onStateChange={handleStateChange}
@ -1688,6 +1834,8 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
sortModel={columnSortModel} sortModel={columnSortModel}
getRowClassName={(params) => (params.indexRelativeToCurrentPage % 2 === 0 ? "even" : "odd")} getRowClassName={(params) => (params.indexRelativeToCurrentPage % 2 === 0 ? "even" : "odd")}
getRowId={(row) => row.__rowIndex} getRowId={(row) => row.__rowIndex}
selectionModel={rowSelectionModel}
hideFooterSelectedRowCount={true}
/> />
</Box> </Box>
</Card> </Card>
@ -1724,4 +1872,49 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
); );
} }
//////////////////////////////////////////////////////////////////////////////////
// mini-component that is the dialog for the user to enter the selection-subset //
//////////////////////////////////////////////////////////////////////////////////
function SelectionSubsetDialog(props: {isOpen: boolean; initialValue: number; closeHandler: (value?: number) => void})
{
const [value, setValue] = useState(props.initialValue)
const handleChange = (newValue: string) =>
{
setValue(parseInt(newValue))
}
const keyPressed = (e: React.KeyboardEvent<HTMLDivElement>) =>
{
if(e.key == "Enter" && value)
{
props.closeHandler(value);
}
}
return (
<Dialog open={props.isOpen} onClose={() => props.closeHandler()} onKeyPress={(e) => keyPressed(e)}>
<DialogTitle>Subset of the Query Result</DialogTitle>
<DialogContent>
<DialogContentText>How many records do you want to select?</DialogContentText>
<TextField
autoFocus
name="selection-subset-size"
inputProps={{width: "100%", type: "number", min: 1}}
onChange={(e) => handleChange(e.target.value)}
value={value}
sx={{width: "100%"}}
onFocus={event => event.target.select()}
/>
</DialogContent>
<DialogActions>
<QCancelButton disabled={false} onClickHandler={() => props.closeHandler()} />
<QSaveButton label="OK" iconName="check" disabled={value == undefined || isNaN(value)} onClickHandler={() => props.closeHandler(value)} />
</DialogActions>
</Dialog>
)
}
export default RecordQuery; export default RecordQuery;

View File

@ -124,7 +124,6 @@
.MuiDataGrid-toolbarContainer .selectionTool .MuiDataGrid-toolbarContainer .selectionTool
{ {
margin-left: 40px;
font-size: 14px; font-size: 14px;
} }

View File

@ -22,10 +22,12 @@
import {AdornmentType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/AdornmentType"; import {AdornmentType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/AdornmentType";
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData"; import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType"; import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType";
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData"; import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord"; import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
import {getGridDateOperators, GridColDef, GridRowsProp} from "@mui/x-data-grid-pro"; import {getGridDateOperators, GridColDef, GridRowsProp} from "@mui/x-data-grid-pro";
import {GridFilterOperator} from "@mui/x-data-grid/models/gridFilterOperator"; import {GridFilterOperator} from "@mui/x-data-grid/models/gridFilterOperator";
import React from "react";
import {Link} from "react-router-dom"; import {Link} from "react-router-dom";
import {buildQGridPvsOperators, QGridBooleanOperators, QGridNumericOperators, QGridStringOperators} from "qqq/pages/records/query/GridFilterOperators"; import {buildQGridPvsOperators, QGridBooleanOperators, QGridNumericOperators, QGridStringOperators} from "qqq/pages/records/query/GridFilterOperators";
import ValueUtils from "qqq/utils/qqq/ValueUtils"; import ValueUtils from "qqq/utils/qqq/ValueUtils";
@ -87,7 +89,7 @@ export default class DataGridUtils
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
public static setupGridColumns = (tableMetaData: QTableMetaData, linkBase: string = ""): GridColDef[] => public static setupGridColumns = (tableMetaData: QTableMetaData, linkBase: string = "", metaData?: QInstance): GridColDef[] =>
{ {
const columns = [] as GridColDef[]; const columns = [] as GridColDef[];
this.addColumnsForTable(tableMetaData, linkBase, columns, null); this.addColumnsForTable(tableMetaData, linkBase, columns, null);
@ -97,8 +99,15 @@ export default class DataGridUtils
for (let i = 0; i < tableMetaData.exposedJoins.length; i++) for (let i = 0; i < tableMetaData.exposedJoins.length; i++)
{ {
const join = tableMetaData.exposedJoins[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 + ": "); let joinLinkBase = null;
if(metaData)
{
joinLinkBase = metaData.getTablePath(join.joinTable);
joinLinkBase += joinLinkBase.endsWith("/") ? "" : "/";
}
this.addColumnsForTable(join.joinTable, joinLinkBase, columns, join.joinTable.name + ".", join.label + ": ");
} }
} }
@ -111,6 +120,10 @@ export default class DataGridUtils
*******************************************************************************/ *******************************************************************************/
private static addColumnsForTable(tableMetaData: QTableMetaData, linkBase: string, columns: GridColDef[], namePrefix?: string, labelPrefix?: string) private static addColumnsForTable(tableMetaData: QTableMetaData, linkBase: string, columns: GridColDef[], namePrefix?: string, labelPrefix?: string)
{ {
/*
////////////////////////////////////////////////////////////////////////
// this sorted by sections - e.g., manual sorting by the meta-data... //
////////////////////////////////////////////////////////////////////////
const sortedKeys: string[] = []; const sortedKeys: string[] = [];
for (let i = 0; i < tableMetaData.sections.length; i++) for (let i = 0; i < tableMetaData.sections.length; i++)
{ {
@ -125,23 +138,37 @@ export default class DataGridUtils
sortedKeys.push(section.fieldNames[j]); sortedKeys.push(section.fieldNames[j]);
} }
} }
*/
///////////////////////////
// sort by labels... mmm //
///////////////////////////
const sortedKeys = [...tableMetaData.fields.keys()];
sortedKeys.sort((a: string, b: string): number =>
{
return (tableMetaData.fields.get(a).label.localeCompare(tableMetaData.fields.get(b).label))
})
sortedKeys.forEach((key) => sortedKeys.forEach((key) =>
{ {
const field = tableMetaData.fields.get(key); const field = tableMetaData.fields.get(key);
const column = this.makeColumnFromField(field, tableMetaData, namePrefix, labelPrefix); const column = this.makeColumnFromField(field, tableMetaData, namePrefix, labelPrefix);
if (key === tableMetaData.primaryKeyField && linkBase) if(key === tableMetaData.primaryKeyField && linkBase && namePrefix == null)
{ {
columns.splice(0, 0, column); columns.splice(0, 0, column);
column.renderCell = (cellValues: any) => (
<Link to={`${linkBase}${cellValues.value}`}>{cellValues.value}</Link>
);
} }
else else
{ {
columns.push(column); columns.push(column);
} }
if (key === tableMetaData.primaryKeyField && linkBase)
{
column.renderCell = (cellValues: any) => (
<Link to={`${linkBase}${cellValues.value}`} onClick={(e) => e.stopPropagation()}>{cellValues.value}</Link>
);
}
}); });
} }