CTLE-436: added variant selection when querying or viewing records

This commit is contained in:
Tim Chamberlain
2023-06-28 19:33:44 -05:00
parent a6425eef9f
commit 690a7a1cae
4 changed files with 160 additions and 18 deletions

View File

@ -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.72",
"@kingsrook/qqq-frontend-core": "1.0.73",
"@mui/icons-material": "5.4.1",
"@mui/material": "5.11.1",
"@mui/styles": "5.11.1",

View File

@ -25,12 +25,14 @@ import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QF
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData";
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import {QTableVariant} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableVariant";
import {QJobComplete} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobComplete";
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 {Alert, Collapse, TablePagination, Typography} from "@mui/material";
import Autocomplete from "@mui/material/Autocomplete";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import Card from "@mui/material/Card";
@ -78,6 +80,7 @@ const ROWS_PER_PAGE_LOCAL_STORAGE_KEY_ROOT = "qqq.rowsPerPage";
const PINNED_COLUMNS_LOCAL_STORAGE_KEY_ROOT = "qqq.pinnedColumns";
const SEEN_JOIN_TABLES_LOCAL_STORAGE_KEY_ROOT = "qqq.seenJoinTables";
const DENSITY_LOCAL_STORAGE_KEY_ROOT = "qqq.density";
const TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT = "qqq.tableVariant";
interface Props
{
@ -134,6 +137,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
const seenJoinTablesLocalStorageKey = `${SEEN_JOIN_TABLES_LOCAL_STORAGE_KEY_ROOT}.${tableName}`;
const columnVisibilityLocalStorageKey = `${COLUMN_VISIBILITY_LOCAL_STORAGE_KEY_ROOT}.${tableName}`;
const filterLocalStorageKey = `${FILTER_LOCAL_STORAGE_KEY_ROOT}.${tableName}`;
const tableVariantLocalStorageKey = `${TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT}.${tableName}`;
let defaultSort = [] as GridSortItem[];
let defaultVisibility = {} as { [index: string]: boolean };
let didDefaultVisibilityComeFromLocalStorage = false;
@ -141,6 +145,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
let defaultDensity = "standard" as GridDensity;
let defaultPinnedColumns = {left: ["__check__", "id"]} as GridPinnedColumns;
let seenJoinTables: {[tableName: string]: boolean} = {};
let defaultTableVariant: QTableVariant = null;
////////////////////////////////////////////////////////////////////////////////////
// set the to be not per table (do as above if we want per table) at a later port //
@ -172,11 +177,16 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
{
seenJoinTables = JSON.parse(localStorage.getItem(seenJoinTablesLocalStorageKey));
}
if (localStorage.getItem(tableVariantLocalStorageKey))
{
defaultTableVariant = JSON.parse(localStorage.getItem(tableVariantLocalStorageKey));
}
const [filterModel, setFilterModel] = useState({items: []} as GridFilterModel);
const [lastFetchedQFilterJSON, setLastFetchedQFilterJSON] = useState("");
const [columnSortModel, setColumnSortModel] = useState(defaultSort);
const [queryFilter, setQueryFilter] = useState(new QQueryFilter());
const [tableVariant, setTableVariant] = useState(defaultTableVariant);
const [columnVisibilityModel, setColumnVisibilityModel] = useState(defaultVisibility);
const [shouldSetAllNewJoinFieldsToHidden, setShouldSetAllNewJoinFieldsToHidden] = useState(!didDefaultVisibilityComeFromLocalStorage)
@ -204,6 +214,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
const [distinctRecordsOnPageCount, setDistinctRecordsOnPageCount] = useState(null as number);
const [selectionSubsetSize, setSelectionSubsetSize] = useState(null as number);
const [selectionSubsetSizePromptOpen, setSelectionSubsetSizePromptOpen] = useState(false);
const [tableVariantPromptOpen, setTableVariantPromptOpen] = 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[]);
@ -337,6 +348,11 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
}, [location, tableMetaData]);
function promptForTableVariantSelection()
{
setTableVariantPromptOpen(true);
}
const updateColumnVisibilityModel = () =>
{
if (localStorage.getItem(columnVisibilityLocalStorageKey))
@ -423,8 +439,10 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
return (false);
}
const getPageHeader = (tableMetaData: QTableMetaData, visibleJoinTables: Set<string>): string | JSX.Element =>
const getPageHeader = (tableMetaData: QTableMetaData, visibleJoinTables: Set<string>, tableVariant: QTableVariant): string | JSX.Element =>
{
let label: string = tableMetaData?.label ?? "";
if (visibleJoinTables.size > 0)
{
let joinLabels = [];
@ -461,15 +479,36 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
return(
<div>
{tableMetaData?.label}
{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>
{
tableVariant &&
<Typography variant="h6" color="text" fontWeight="light">
{tableMetaData.variantTableLabel}: {tableVariant.name}
<Tooltip title={`Change ${tableMetaData.variantTableLabel}`}>
<IconButton onClick={promptForTableVariantSelection} sx={{p: 0, m: 0, ml: .5, mb: .5, color: "#9f9f9f", fontVariationSettings: "'wght' 100"}}><Icon fontSize="small">settings</Icon></IconButton>
</Tooltip>
</Typography>
}
</div>);
}
else
{
return (tableMetaData?.label);
return (
<div>
{label}
{
tableVariant &&
<Typography variant="h6" color="text" fontWeight="light">
{tableMetaData.variantTableLabel}: {tableVariant.name}
<Tooltip title={`Change ${tableMetaData.variantTableLabel}`}>
<IconButton onClick={promptForTableVariantSelection} sx={{p: 0, m: 0, ml: .5, mb: .5, color: "#9f9f9f", fontVariationSettings: "'wght' 100"}}><Icon fontSize="small">settings</Icon></IconButton>
</Tooltip>
</Typography>
}
</div>);
}
};
@ -480,9 +519,8 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
(async () =>
{
const tableMetaData = await qController.loadTableMetaData(tableName);
const visibleJoinTables = getVisibleJoinTables();
setPageHeader(getPageHeader(tableMetaData, visibleJoinTables));
setPageHeader(getPageHeader(tableMetaData, visibleJoinTables, tableVariant));
////////////////////////////////////////////////////////////////////////////////////////////////////////
// if there's an exposedJoin that we haven't seen before, we want to make sure that all of its fields //
@ -543,6 +581,12 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
setTableMetaData(tableMetaData);
setTableLabel(tableMetaData.label);
if(tableMetaData?.usesVariants && ! tableVariant)
{
promptForTableVariantSelection();
return;
}
if (columnsModel.length == 0)
{
let linkBase = metaData.getTablePath(table);
@ -633,7 +677,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
if (tableMetaData.capabilities.has(Capability.TABLE_COUNT))
{
let includeDistinct = isJoinMany(tableMetaData, getVisibleJoinTables());
qController.count(tableName, qFilter, queryJoins, includeDistinct).then(([count, distinctCount]) =>
qController.count(tableName, qFilter, queryJoins, includeDistinct, tableVariant).then(([count, distinctCount]) =>
{
console.log(`Received count results for query ${thisQueryId}: ${count} ${distinctCount}`);
countResults[thisQueryId] = [];
@ -645,7 +689,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
}
setLastFetchedQFilterJSON(JSON.stringify(qFilter));
qController.query(tableName, qFilter, queryJoins).then((results) =>
qController.query(tableName, qFilter, queryJoins, tableVariant).then((results) =>
{
console.log(`Received results for query ${thisQueryId}`);
queryResults[thisQueryId] = results;
@ -1762,7 +1806,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
setTotalRecords(null);
setDistinctRecords(null);
updateTable();
}, [columnsModel, tableState]);
}, [columnsModel, tableState, tableVariant]);
useEffect(() =>
{
@ -1949,6 +1993,15 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
</Modal>
}
{
tableMetaData &&
<TableVariantDialog table={tableMetaData} isOpen={tableVariantPromptOpen} closeHandler={(value: QTableVariant) =>
{
setTableVariantPromptOpen(false);
setTableVariant(value);
}} />
}
{
columnStatsFieldName &&
<Modal open={columnStatsFieldName !== null} onClose={(event, reason) => closeColumnStats(event, reason)}>
@ -1971,6 +2024,93 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
);
}
////////////////////////////////////////////////////////////////////////////////////////////////////////
// mini-component that is the dialog for the user to select a variant on tables with variant backends //
////////////////////////////////////////////////////////////////////////////////////////////////////////
function TableVariantDialog(props: {isOpen: boolean; table: QTableMetaData; closeHandler: (value?: QTableVariant) => void})
{
const [value, setValue] = useState(null)
const [dropDownOpen, setDropDownOpen] = useState(false)
const [variants, setVariants] = useState(null);
const handleVariantChange = (event: React.SyntheticEvent, value: any | any[], reason: string, details?: string) =>
{
const tableVariantLocalStorageKey = `${TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT}.${props.table.name}`;
if(value != null)
{
localStorage.setItem(tableVariantLocalStorageKey, JSON.stringify(value));
}
else
{
localStorage.removeItem(tableVariantLocalStorageKey);
}
props.closeHandler(value);
};
const keyPressed = (e: React.KeyboardEvent<HTMLDivElement>) =>
{
if(e.key == "Enter" && value)
{
props.closeHandler(value);
}
}
useEffect(() =>
{
console.log("queryVariants")
try
{
(async () =>
{
const variants = await qController.tableVariants(props.table.name);
console.log(JSON.stringify(variants));
setVariants(variants);
})();
}
catch (e)
{
console.log(e);
}
}, []);
return variants && (
<Dialog open={props.isOpen} onKeyPress={(e) => keyPressed(e)}>
<DialogTitle>{props.table.variantTableLabel}</DialogTitle>
<DialogContent>
<DialogContentText>Select the {props.table.variantTableLabel} to be used on this table:</DialogContentText>
<Autocomplete
id="tableVariantId"
sx={{width: "400px", marginTop: "10px"}}
open={dropDownOpen}
size="small"
onOpen={() =>
{
setDropDownOpen(true);
}}
onClose={() =>
{
setDropDownOpen(false);
}}
// @ts-ignore
onChange={handleVariantChange}
isOptionEqualToValue={(option, value) => option.id === value.id}
options={variants}
renderInput={(params) => <TextField {...params} label={props.table.variantTableLabel} />}
getOptionLabel={(option) =>
{
if(typeof option == "object")
{
return (option as QTableVariant).name;
}
return option;
}}
/>
</DialogContent>
</Dialog>
)
}
//////////////////////////////////////////////////////////////////////////////////
// mini-component that is the dialog for the user to enter the selection-subset //
//////////////////////////////////////////////////////////////////////////////////

View File

@ -25,6 +25,7 @@ import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstan
import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData";
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import {QTableSection} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableSection";
import {QTableVariant} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableVariant";
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
import {Alert, Typography} from "@mui/material";
import Avatar from "@mui/material/Avatar";
@ -74,6 +75,8 @@ RecordView.defaultProps =
launchProcess: null,
};
const TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT = "qqq.tableVariant";
function RecordView({table, launchProcess}: Props): JSX.Element
{
const {id} = useParams();
@ -83,7 +86,9 @@ function RecordView({table, launchProcess}: Props): JSX.Element
const pathParts = location.pathname.replace(/\/+$/, "").split("/");
const tableName = table.name;
let tableVariant: QTableVariant = null;
const tableVariantLocalStorageKey = `${TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT}.${tableName}`;
const [asyncLoadInited, setAsyncLoadInited] = useState(false);
const [sectionFieldElements, setSectionFieldElements] = useState(null as Map<string, JSX.Element[]>);
const [adornmentFieldsMap, setAdornmentFieldsMap] = useState(new Map<string, boolean>);
@ -112,7 +117,10 @@ function RecordView({table, launchProcess}: Props): JSX.Element
const {accentColor, setPageHeader, tableMetaData, setTableMetaData, tableProcesses, setTableProcesses, dotMenuOpen} = useContext(QContext);
if (localStorage.getItem(tableVariantLocalStorageKey))
{
tableVariant = JSON.parse(localStorage.getItem(tableVariantLocalStorageKey));
}
const reload = () =>
{
@ -360,7 +368,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element
let record: QRecord;
try
{
record = await qController.get(tableName, id);
record = await qController.get(tableName, id, tableVariant);
setRecord(record);
}
catch (e)

View File

@ -1,9 +1,3 @@
::selection {
background: hotpink;
color: white;
}
html,
body {
padding: 0;