mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-18 13:20:43 +00:00
Checkpoint of joins, new handling of the select-all/page/ and new 'subset' options
This commit is contained in:
@ -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>
|
||||||
|
@ -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">
|
||||||
({distinctRecords} distinct<CustomWidthTooltip title={tooltipHTML}>
|
({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;
|
||||||
|
@ -124,7 +124,6 @@
|
|||||||
|
|
||||||
.MuiDataGrid-toolbarContainer .selectionTool
|
.MuiDataGrid-toolbarContainer .selectionTool
|
||||||
{
|
{
|
||||||
margin-left: 40px;
|
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user