mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-18 05:10:45 +00:00
ADding goto record button/dialog
This commit is contained in:
276
src/qqq/components/misc/GotoRecordDialog.tsx
Normal file
276
src/qqq/components/misc/GotoRecordDialog.tsx
Normal file
@ -0,0 +1,276 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2023. Kingsrook, LLC
|
||||||
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||||
|
* contact@kingsrook.com
|
||||||
|
* https://github.com/Kingsrook/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
|
||||||
|
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
|
||||||
|
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
||||||
|
import {QCriteriaOperator} from "@kingsrook/qqq-frontend-core/lib/model/query/QCriteriaOperator";
|
||||||
|
import {QFilterCriteria} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterCriteria";
|
||||||
|
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Button from "@mui/material/Button";
|
||||||
|
import Dialog from "@mui/material/Dialog";
|
||||||
|
import DialogActions from "@mui/material/DialogActions";
|
||||||
|
import DialogContent from "@mui/material/DialogContent";
|
||||||
|
import DialogTitle from "@mui/material/DialogTitle";
|
||||||
|
import Grid from "@mui/material/Grid";
|
||||||
|
import Icon from "@mui/material/Icon";
|
||||||
|
import TextField from "@mui/material/TextField";
|
||||||
|
import React, {useState} from "react";
|
||||||
|
import {useNavigate} from "react-router-dom";
|
||||||
|
import {QCancelButton} from "qqq/components/buttons/DefaultButtons";
|
||||||
|
import MDButton from "qqq/components/legacy/MDButton";
|
||||||
|
import Client from "qqq/utils/qqq/Client";
|
||||||
|
|
||||||
|
interface Props
|
||||||
|
{
|
||||||
|
isOpen: boolean;
|
||||||
|
metaData: QInstance;
|
||||||
|
tableMetaData: QTableMetaData;
|
||||||
|
closeHandler: () => void;
|
||||||
|
mayClose: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
GotoRecordDialog.defaultProps = {
|
||||||
|
mayClose: true
|
||||||
|
};
|
||||||
|
|
||||||
|
const qController = Client.getInstance();
|
||||||
|
|
||||||
|
function hasGotoFieldNames(tableMetaData: QTableMetaData): boolean
|
||||||
|
{
|
||||||
|
const mdbMetaData = tableMetaData?.supplementalTableMetaData?.get("materialDashboard");
|
||||||
|
if(mdbMetaData && mdbMetaData.gotoFieldNames)
|
||||||
|
{
|
||||||
|
return (true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function GotoRecordDialog(props: Props): JSX.Element
|
||||||
|
{
|
||||||
|
const fields: QFieldMetaData[] = []
|
||||||
|
|
||||||
|
let pkey = props?.tableMetaData?.fields.get(props?.tableMetaData?.primaryKeyField);
|
||||||
|
let addedPkey = false;
|
||||||
|
const mdbMetaData = props?.tableMetaData?.supplementalTableMetaData?.get("materialDashboard");
|
||||||
|
if(mdbMetaData)
|
||||||
|
{
|
||||||
|
if(mdbMetaData.gotoFieldNames)
|
||||||
|
{
|
||||||
|
for(let i = 0; i<mdbMetaData.gotoFieldNames.length; i++)
|
||||||
|
{
|
||||||
|
// todo - multi-field keys!!
|
||||||
|
let fieldName = mdbMetaData.gotoFieldNames[i][0];
|
||||||
|
let field = props.tableMetaData.fields.get(fieldName);
|
||||||
|
if(field)
|
||||||
|
{
|
||||||
|
fields.push(field);
|
||||||
|
|
||||||
|
if(field.name == pkey.name)
|
||||||
|
{
|
||||||
|
addedPkey = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(pkey && !addedPkey)
|
||||||
|
{
|
||||||
|
fields.unshift(pkey);
|
||||||
|
}
|
||||||
|
|
||||||
|
const makeInitialValues = () =>
|
||||||
|
{
|
||||||
|
const rs = {} as {[field: string]: string};
|
||||||
|
fields.forEach((field) => rs[field.name] = "");
|
||||||
|
return (rs);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
const [values, setValues] = useState(makeInitialValues());
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleChange = (fieldName: string, newValue: string) =>
|
||||||
|
{
|
||||||
|
values[fieldName] = newValue;
|
||||||
|
setValues(JSON.parse(JSON.stringify(values)));
|
||||||
|
}
|
||||||
|
|
||||||
|
const close = () =>
|
||||||
|
{
|
||||||
|
setError("");
|
||||||
|
setValues(makeInitialValues());
|
||||||
|
props.closeHandler();
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyPressed = (e: React.KeyboardEvent<HTMLDivElement>) =>
|
||||||
|
{
|
||||||
|
// @ts-ignore
|
||||||
|
const targetId: string = e.target?.id;
|
||||||
|
|
||||||
|
if(e.key == "Esc")
|
||||||
|
{
|
||||||
|
if(props.mayClose)
|
||||||
|
{
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if(e.key == "Enter" && targetId?.startsWith("gotoInput-"))
|
||||||
|
{
|
||||||
|
const index = targetId?.replaceAll("gotoInput-", "");
|
||||||
|
document.getElementById("gotoButton-" + index).click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeRequested = () =>
|
||||||
|
{
|
||||||
|
if(props.mayClose)
|
||||||
|
{
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const goClicked = async (fieldName: string) =>
|
||||||
|
{
|
||||||
|
setError("");
|
||||||
|
const filter = new QQueryFilter([new QFilterCriteria(fieldName, QCriteriaOperator.EQUALS, [values[fieldName]])], null, "AND", null, 10);
|
||||||
|
const queryResult = await qController.query(props.tableMetaData.name, filter)
|
||||||
|
if(queryResult.length == 0)
|
||||||
|
{
|
||||||
|
setError("Record not found.");
|
||||||
|
setTimeout(() => setError(""), 3000);
|
||||||
|
}
|
||||||
|
else if(queryResult.length == 1)
|
||||||
|
{
|
||||||
|
navigate(`${props.metaData.getTablePathByName(props.tableMetaData.name)}/${queryResult[0].values.get(props.tableMetaData.primaryKeyField)}`);
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
setError("More than 1 record found...");
|
||||||
|
setTimeout(() => setError(""), 3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(props.tableMetaData)
|
||||||
|
{
|
||||||
|
if (fields.length == 0 && !error)
|
||||||
|
{
|
||||||
|
setError("This table is not configured for this feature.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={props.isOpen} onClose={() => closeRequested} onKeyPress={(e) => keyPressed(e)} fullWidth maxWidth={"sm"}>
|
||||||
|
<DialogTitle>Go To...</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
{
|
||||||
|
fields.map((field, index) =>
|
||||||
|
(
|
||||||
|
<Grid key={field.name} container alignItems="center" py={1}>
|
||||||
|
<Grid item xs={3} textAlign="right" pr={2}>
|
||||||
|
{field.label}
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={6}>
|
||||||
|
<TextField
|
||||||
|
id={`gotoInput-${index}`}
|
||||||
|
autoFocus={index == 0}
|
||||||
|
autoComplete="off"
|
||||||
|
inputProps={{width: "100%"}}
|
||||||
|
onChange={(e) => handleChange(field.name, e.target.value)}
|
||||||
|
value={values[field.name]}
|
||||||
|
sx={{width: "100%"}}
|
||||||
|
onFocus={event => event.target.select()}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={1} pl={2}>
|
||||||
|
<MDButton id={`gotoButton-${index}`} type="submit" variant="gradient" color="info" size="small" onClick={() => goClicked(field.name)} fullWidth startIcon={<Icon>double_arrow</Icon>} disabled={`${values[field.name]}`.length == 0}>
|
||||||
|
Go
|
||||||
|
</MDButton>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
{
|
||||||
|
error &&
|
||||||
|
<Box color="red">
|
||||||
|
{error}
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
</DialogContent>
|
||||||
|
{
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// show the cancel button if allowed - else we need a little spacing, so an empty box //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
props.mayClose ?
|
||||||
|
<DialogActions>
|
||||||
|
<QCancelButton disabled={false} onClickHandler={close} label="Close" />
|
||||||
|
</DialogActions>
|
||||||
|
: <Box> </Box>
|
||||||
|
}
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GotoRecordButtonProps
|
||||||
|
{
|
||||||
|
metaData: QInstance;
|
||||||
|
tableMetaData: QTableMetaData;
|
||||||
|
autoOpen?: boolean;
|
||||||
|
buttonVisible?: boolean;
|
||||||
|
mayClose?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
GotoRecordButton.defaultProps = {
|
||||||
|
autoOpen: false,
|
||||||
|
buttonVisible: true,
|
||||||
|
mayClose: true
|
||||||
|
};
|
||||||
|
|
||||||
|
export function GotoRecordButton(props: GotoRecordButtonProps): JSX.Element
|
||||||
|
{
|
||||||
|
const [gotoIsOpen, setGotoIsOpen] = useState(props.autoOpen)
|
||||||
|
|
||||||
|
function openGoto()
|
||||||
|
{
|
||||||
|
setGotoIsOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeGoto()
|
||||||
|
{
|
||||||
|
setGotoIsOpen(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
{
|
||||||
|
props.buttonVisible && hasGotoFieldNames(props.tableMetaData) && <Button onClick={openGoto} >Go To...</Button>
|
||||||
|
}
|
||||||
|
<GotoRecordDialog metaData={props.metaData} tableMetaData={props.tableMetaData} isOpen={gotoIsOpen} closeHandler={closeGoto} mayClose={props.mayClose} />
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GotoRecordDialog;
|
@ -57,6 +57,7 @@ import {useLocation, useNavigate, useSearchParams} from "react-router-dom";
|
|||||||
import QContext from "QContext";
|
import QContext from "QContext";
|
||||||
import {QActionsMenuButton, QCancelButton, QCreateNewButton, QSaveButton} from "qqq/components/buttons/DefaultButtons";
|
import {QActionsMenuButton, QCancelButton, QCreateNewButton, QSaveButton} from "qqq/components/buttons/DefaultButtons";
|
||||||
import MenuButton from "qqq/components/buttons/MenuButton";
|
import MenuButton from "qqq/components/buttons/MenuButton";
|
||||||
|
import {GotoRecordButton} from "qqq/components/misc/GotoRecordDialog";
|
||||||
import SavedFilters from "qqq/components/misc/SavedFilters";
|
import SavedFilters from "qqq/components/misc/SavedFilters";
|
||||||
import {CustomColumnsPanel} from "qqq/components/query/CustomColumnsPanel";
|
import {CustomColumnsPanel} from "qqq/components/query/CustomColumnsPanel";
|
||||||
import {CustomFilterPanel} from "qqq/components/query/CustomFilterPanel";
|
import {CustomFilterPanel} from "qqq/components/query/CustomFilterPanel";
|
||||||
@ -644,6 +645,12 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(!tableMetaData.capabilities.has(Capability.TABLE_QUERY))
|
||||||
|
{
|
||||||
|
console.log("Cannot update table - it does not have QUERY capability.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setLastFetchedQFilterJSON(JSON.stringify(qFilter));
|
setLastFetchedQFilterJSON(JSON.stringify(qFilter));
|
||||||
qController.query(tableName, qFilter, queryJoins).then((results) =>
|
qController.query(tableName, qFilter, queryJoins).then((results) =>
|
||||||
{
|
{
|
||||||
@ -1803,6 +1810,19 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// if the table doesn't allow QUERY, but does allow GET, don't render a data grid - //
|
||||||
|
// instead, try to just render a Goto Record button, in auto-open, and may-not-close modes //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
if (tableMetaData && !tableMetaData.capabilities.has(Capability.TABLE_QUERY) && tableMetaData.capabilities.has(Capability.TABLE_GET))
|
||||||
|
{
|
||||||
|
return (
|
||||||
|
<BaseLayout>
|
||||||
|
<GotoRecordButton metaData={metaData} tableMetaData={tableMetaData} autoOpen={true} buttonVisible={false} mayClose={false} />
|
||||||
|
</BaseLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseLayout>
|
<BaseLayout>
|
||||||
<div className="recordQuery">
|
<div className="recordQuery">
|
||||||
@ -1857,6 +1877,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
}
|
}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
<GotoRecordButton metaData={metaData} tableMetaData={tableMetaData} />
|
||||||
<Box display="flex" width="150px">
|
<Box display="flex" width="150px">
|
||||||
<QActionsMenuButton isOpen={actionsMenu} onClickHandler={openActionsMenu} />
|
<QActionsMenuButton isOpen={actionsMenu} onClickHandler={openActionsMenu} />
|
||||||
{renderActionsMenu}
|
{renderActionsMenu}
|
||||||
|
@ -48,6 +48,7 @@ import QContext from "QContext";
|
|||||||
import AuditBody from "qqq/components/audits/AuditBody";
|
import AuditBody from "qqq/components/audits/AuditBody";
|
||||||
import {QActionsMenuButton, QCancelButton, QDeleteButton, QEditButton} from "qqq/components/buttons/DefaultButtons";
|
import {QActionsMenuButton, QCancelButton, QDeleteButton, QEditButton} from "qqq/components/buttons/DefaultButtons";
|
||||||
import EntityForm from "qqq/components/forms/EntityForm";
|
import EntityForm from "qqq/components/forms/EntityForm";
|
||||||
|
import {GotoRecordButton} from "qqq/components/misc/GotoRecordDialog";
|
||||||
import QRecordSidebar from "qqq/components/misc/RecordSidebar";
|
import QRecordSidebar from "qqq/components/misc/RecordSidebar";
|
||||||
import DashboardWidgets from "qqq/components/widgets/DashboardWidgets";
|
import DashboardWidgets from "qqq/components/widgets/DashboardWidgets";
|
||||||
import BaseLayout from "qqq/layouts/BaseLayout";
|
import BaseLayout from "qqq/layouts/BaseLayout";
|
||||||
@ -739,7 +740,10 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
|||||||
<Typography variant="h5">
|
<Typography variant="h5">
|
||||||
{tableMetaData && record ? `Viewing ${tableMetaData?.label}: ${record?.recordLabel || ""}` : ""}
|
{tableMetaData && record ? `Viewing ${tableMetaData?.label}: ${record?.recordLabel || ""}` : ""}
|
||||||
</Typography>
|
</Typography>
|
||||||
<QActionsMenuButton isOpen={actionsMenu} onClickHandler={openActionsMenu} />
|
<Box display="flex">
|
||||||
|
<GotoRecordButton metaData={metaData} tableMetaData={tableMetaData} />
|
||||||
|
<QActionsMenuButton isOpen={actionsMenu} onClickHandler={openActionsMenu} />
|
||||||
|
</Box>
|
||||||
{renderActionsMenu}
|
{renderActionsMenu}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
Reference in New Issue
Block a user