mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-21 14:48:43 +00:00
Compare commits
9 Commits
snapshot-f
...
snapshot-f
Author | SHA1 | Date | |
---|---|---|---|
0831a87674 | |||
dd5cd459ce | |||
c200cc9fab | |||
17f378131d | |||
376a7a342e | |||
fcadea3192 | |||
086ab775fc | |||
5693661d20 | |||
8c9224aceb |
@ -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.99",
|
||||
"@kingsrook/qqq-frontend-core": "1.0.100",
|
||||
"@mui/icons-material": "5.4.1",
|
||||
"@mui/material": "5.11.1",
|
||||
"@mui/styles": "5.11.1",
|
||||
|
@ -49,6 +49,7 @@ import EntityEdit from "qqq/pages/records/edit/RecordEdit";
|
||||
import RecordQuery from "qqq/pages/records/query/RecordQuery";
|
||||
import RecordDeveloperView from "qqq/pages/records/view/RecordDeveloperView";
|
||||
import RecordView from "qqq/pages/records/view/RecordView";
|
||||
import RecordViewByUniqueKey from "qqq/pages/records/view/RecordViewByUniqueKey";
|
||||
import GoogleAnalyticsUtils, {AnalyticsModel} from "qqq/utils/GoogleAnalyticsUtils";
|
||||
import Client from "qqq/utils/qqq/Client";
|
||||
import ProcessUtils from "qqq/utils/qqq/ProcessUtils";
|
||||
@ -392,6 +393,13 @@ export default function App()
|
||||
component: <RecordView table={table} />,
|
||||
});
|
||||
|
||||
routeList.push({
|
||||
name: `${app.label} View`,
|
||||
key: `${app.name}.view`,
|
||||
route: `${path}/key`,
|
||||
component: <RecordViewByUniqueKey table={table} />,
|
||||
});
|
||||
|
||||
routeList.push({
|
||||
name: `${app.label}`,
|
||||
key: `${app.name}.edit`,
|
||||
|
@ -35,6 +35,7 @@ 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 {any} from "prop-types";
|
||||
import React, {useState} from "react";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
import {QCancelButton} from "qqq/components/buttons/DefaultButtons";
|
||||
@ -71,7 +72,12 @@ function hasGotoFieldNames(tableMetaData: QTableMetaData): boolean
|
||||
|
||||
function GotoRecordDialog(props: Props): JSX.Element
|
||||
{
|
||||
const fields: QFieldMetaData[] = [];
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// this is an array of array of fields. //
|
||||
// that is - each entry in the top-level array is a set of fields that can be used together to goto a record //
|
||||
// such as (pkey), (ukey-field1,ukey-field2). //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
const options: QFieldMetaData[][] = [];
|
||||
|
||||
let pkey = props?.tableMetaData?.fields.get(props?.tableMetaData?.primaryKeyField);
|
||||
let addedPkey = false;
|
||||
@ -82,31 +88,38 @@ function GotoRecordDialog(props: Props): JSX.Element
|
||||
{
|
||||
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)
|
||||
const option: QFieldMetaData[] = [];
|
||||
options.push(option);
|
||||
for (let j = 0; j < mdbMetaData.gotoFieldNames[i].length; j++)
|
||||
{
|
||||
fields.push(field);
|
||||
|
||||
if (field.name == pkey.name)
|
||||
let fieldName = mdbMetaData.gotoFieldNames[i][j];
|
||||
let field = props.tableMetaData.fields.get(fieldName);
|
||||
if (field)
|
||||
{
|
||||
addedPkey = true;
|
||||
option.push(field);
|
||||
|
||||
if (pkey != null && field.name == pkey.name)
|
||||
{
|
||||
addedPkey = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if pkey wasn't in the gotoField options meta-data, go ahead add it as an option here //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
if (pkey && !addedPkey)
|
||||
{
|
||||
fields.unshift(pkey);
|
||||
options.unshift([pkey]);
|
||||
}
|
||||
|
||||
const makeInitialValues = () =>
|
||||
{
|
||||
const rs = {} as { [field: string]: string };
|
||||
fields.forEach((field) => rs[field.name] = "");
|
||||
options.forEach((option) => option.forEach((field) => rs[field.name] = ""));
|
||||
return (rs);
|
||||
};
|
||||
|
||||
@ -141,11 +154,16 @@ function GotoRecordDialog(props: Props): JSX.Element
|
||||
}
|
||||
else if (e.key == "Enter" && targetId?.startsWith("gotoInput-"))
|
||||
{
|
||||
const index = targetId?.replaceAll("gotoInput-", "");
|
||||
const parts = targetId?.split(/-/);
|
||||
const index = parts[1];
|
||||
document.getElementById("gotoButton-" + index).click();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** event handler for close button
|
||||
***************************************************************************/
|
||||
const closeRequested = () =>
|
||||
{
|
||||
if (props.mayClose)
|
||||
@ -154,10 +172,47 @@ function GotoRecordDialog(props: Props): JSX.Element
|
||||
}
|
||||
};
|
||||
|
||||
const goClicked = async (fieldName: string) =>
|
||||
|
||||
/*******************************************************************************
|
||||
** function to say if an option's submit button should be disabled
|
||||
*******************************************************************************/
|
||||
const isOptionSubmitButtonDisabled = (optionIndex: number) =>
|
||||
{
|
||||
let anyFieldsInThisOptionHaveAValue = false;
|
||||
|
||||
options[optionIndex].forEach((field) =>
|
||||
{
|
||||
if(values[field.name])
|
||||
{
|
||||
anyFieldsInThisOptionHaveAValue = true;
|
||||
}
|
||||
})
|
||||
|
||||
if(!anyFieldsInThisOptionHaveAValue)
|
||||
{
|
||||
return (true);
|
||||
}
|
||||
return (false);
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** event handler for clicking an 'option's go/submit button
|
||||
***************************************************************************/
|
||||
const optionGoClicked = async (optionIndex: number) =>
|
||||
{
|
||||
setError("");
|
||||
const filter = new QQueryFilter([new QFilterCriteria(fieldName, QCriteriaOperator.EQUALS, [values[fieldName]])], null, null, "AND", null, 10);
|
||||
|
||||
const criteria: QFilterCriteria[] = [];
|
||||
const queryStringParts: string[] = [];
|
||||
options[optionIndex].forEach((field) =>
|
||||
{
|
||||
criteria.push(new QFilterCriteria(field.name, QCriteriaOperator.EQUALS, [values[field.name]]))
|
||||
queryStringParts.push(`${field.name}=${encodeURIComponent(values[field.name])}`)
|
||||
})
|
||||
|
||||
const filter = new QQueryFilter(criteria, null, null, "AND", null, 10);
|
||||
|
||||
try
|
||||
{
|
||||
const queryResult = await qController.query(props.tableMetaData.name, filter, null, props.tableVariant);
|
||||
@ -168,12 +223,26 @@ function GotoRecordDialog(props: Props): JSX.Element
|
||||
}
|
||||
else if (queryResult.length == 1)
|
||||
{
|
||||
navigate(`${props.metaData.getTablePathByName(props.tableMetaData.name)}/${encodeURIComponent(queryResult[0].values.get(props.tableMetaData.primaryKeyField))}`);
|
||||
if(options[optionIndex].length == 1 && options[optionIndex][0].name == pkey?.name)
|
||||
{
|
||||
/////////////////////////////////////////////////
|
||||
// navigate by pkey, if that's how we searched //
|
||||
/////////////////////////////////////////////////
|
||||
navigate(`${props.metaData.getTablePathByName(props.tableMetaData.name)}/${encodeURIComponent(queryResult[0].values.get(props.tableMetaData.primaryKeyField))}`);
|
||||
}
|
||||
else
|
||||
{
|
||||
/////////////////////////////////
|
||||
// else navigate by unique-key //
|
||||
/////////////////////////////////
|
||||
navigate(`${props.metaData.getTablePathByName(props.tableMetaData.name)}/key/?${queryStringParts.join("&")}`);
|
||||
}
|
||||
|
||||
close();
|
||||
}
|
||||
else
|
||||
{
|
||||
setError("More than 1 record found...");
|
||||
setError("More than 1 record was found...");
|
||||
setTimeout(() => setError(""), 3000);
|
||||
}
|
||||
}
|
||||
@ -187,7 +256,7 @@ function GotoRecordDialog(props: Props): JSX.Element
|
||||
|
||||
if (props.tableMetaData)
|
||||
{
|
||||
if (fields.length == 0 && !error)
|
||||
if (options.length == 0 && !error)
|
||||
{
|
||||
setError("This table is not configured for this feature.");
|
||||
}
|
||||
@ -200,31 +269,38 @@ function GotoRecordDialog(props: Props): JSX.Element
|
||||
<DialogContent>
|
||||
{props.subHeader}
|
||||
{
|
||||
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>
|
||||
))
|
||||
options.map((option, optionIndex) =>
|
||||
<Box key={optionIndex}>
|
||||
{
|
||||
option.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-${optionIndex}-${index}`}
|
||||
autoFocus={optionIndex == 0 && 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}>
|
||||
{
|
||||
(index == option.length - 1) &&
|
||||
<MDButton id={`gotoButton-${optionIndex}`} type="submit" variant="gradient" color="info" size="small" onClick={() => optionGoClicked(optionIndex)} fullWidth startIcon={<Icon>double_arrow</Icon>} disabled={isOptionSubmitButtonDisabled(optionIndex)}>Go</MDButton>
|
||||
}
|
||||
</Grid>
|
||||
</Grid>
|
||||
))
|
||||
}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
{
|
||||
error &&
|
||||
|
@ -35,8 +35,7 @@ import {QJobRunning} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJob
|
||||
import {QJobStarted} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobStarted";
|
||||
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
||||
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
|
||||
import {Alert, Button, CircularProgress, Icon, TablePagination} from "@mui/material";
|
||||
import Box from "@mui/material/Box";
|
||||
import {Alert, Box, Button, CircularProgress, Icon, TablePagination} from "@mui/material";
|
||||
import Card from "@mui/material/Card";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import Step from "@mui/material/Step";
|
||||
@ -48,12 +47,14 @@ import FormData from "form-data";
|
||||
import {Form, Formik} from "formik";
|
||||
import parse from "html-react-parser";
|
||||
import QContext from "QContext";
|
||||
import colors from "qqq/assets/theme/base/colors";
|
||||
import {QCancelButton, QSubmitButton} from "qqq/components/buttons/DefaultButtons";
|
||||
import QDynamicForm from "qqq/components/forms/DynamicForm";
|
||||
import DynamicFormUtils from "qqq/components/forms/DynamicFormUtils";
|
||||
import MDButton from "qqq/components/legacy/MDButton";
|
||||
import MDProgress from "qqq/components/legacy/MDProgress";
|
||||
import MDTypography from "qqq/components/legacy/MDTypography";
|
||||
import HelpContent from "qqq/components/misc/HelpContent";
|
||||
import QRecordSidebar from "qqq/components/misc/RecordSidebar";
|
||||
import {GoogleDriveFolderPickerWrapper} from "qqq/components/processes/GoogleDriveFolderPickerWrapper";
|
||||
import ProcessSummaryResults from "qqq/components/processes/ProcessSummaryResults";
|
||||
@ -87,6 +88,14 @@ const INITIAL_RETRY_MILLIS = 1_500;
|
||||
const RETRY_MAX_MILLIS = 12_000;
|
||||
const BACKOFF_AMOUNT = 1.5;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// define a function that we can make referenes to, which we'll overwrite //
|
||||
// with formik's setFieldValue function, once we're inside formik. //
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
let formikSetFieldValueFunction = (field: string, value: any, shouldValidate?: boolean): void =>
|
||||
{
|
||||
}
|
||||
|
||||
function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, isReport, recordIds, closeModalHandler, forceReInit, overrideLabel}: Props): JSX.Element
|
||||
{
|
||||
const processNameParam = useParams().processName;
|
||||
@ -446,6 +455,15 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
});
|
||||
}
|
||||
|
||||
// todo not commit - not ready - need process (or screen) meta-data to have helpContents...
|
||||
/*
|
||||
///////////////////////////////
|
||||
// screen-level help content //
|
||||
///////////////////////////////
|
||||
let helpRoles = ["PROCESS_SCREEN", "ALL_SCREENS"];
|
||||
const formattedHelpContent = <HelpContent helpContents={process.helpContents} roles={helpRoles} helpContentKey={`table:${tableName};section:${section.name}`} />;
|
||||
*/
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
@ -460,6 +478,16 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
</MDTypography>
|
||||
}
|
||||
|
||||
{
|
||||
/*
|
||||
// todo not commit - not ready - need process (or screen) meta-data to have helpContents...
|
||||
formattedHelpContent &&
|
||||
<Box px={"1.5rem"} fontSize={"0.875rem"} color={colors.blueGray.main}>
|
||||
{formattedHelpContent}
|
||||
</Box>
|
||||
*/
|
||||
}
|
||||
|
||||
{
|
||||
//////////////////////////////////////////////////
|
||||
// render all of the components for this screen //
|
||||
@ -472,6 +500,23 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
helpRoles = ["EDIT_SCREEN", "WRITE_SCREENS", "ALL_SCREENS"];
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// if the component specifies a sub-set of field names to include, then //
|
||||
// edit the formData object to just include those. //
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
let formDataToUse = formData;
|
||||
if(component.values && component.values.includeFieldNames)
|
||||
{
|
||||
formDataToUse = Object.assign({}, formData);
|
||||
|
||||
formDataToUse.formFields = {};
|
||||
for (let i = 0; i < component.values.includeFieldNames.length; i++)
|
||||
{
|
||||
const fieldName = component.values.includeFieldNames[i];
|
||||
formDataToUse.formFields[fieldName] = formData.formFields[fieldName];
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={index}>
|
||||
{
|
||||
@ -567,9 +612,22 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
)
|
||||
}
|
||||
{
|
||||
component.type === QComponentType.EDIT_FORM && (
|
||||
<QDynamicForm formData={formData} helpRoles={helpRoles} helpContentKeyPrefix={`process:${processName};`} />
|
||||
)
|
||||
component.type === QComponentType.EDIT_FORM &&
|
||||
<>
|
||||
{
|
||||
component.values?.sectionLabel ?
|
||||
<Box py={1.5}>
|
||||
<Card sx={{scrollMarginTop: "20px"}}>
|
||||
<MDTypography variant="h5" p={3} pl={2} pb={1}>
|
||||
{component.values?.sectionLabel}
|
||||
</MDTypography>
|
||||
<Box pt={0} p={2}>
|
||||
<QDynamicForm formData={formDataToUse} helpRoles={helpRoles} helpContentKeyPrefix={`process:${processName};`} />
|
||||
</Box>
|
||||
</Card>
|
||||
</Box> : <QDynamicForm formData={formDataToUse} helpRoles={helpRoles} helpContentKeyPrefix={`process:${processName};`} />
|
||||
}
|
||||
</>
|
||||
}
|
||||
{
|
||||
component.type === QComponentType.VIEW_FORM && step.viewFields && (
|
||||
@ -1026,6 +1084,26 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
setProcessValues(qJobComplete.values);
|
||||
setQJobRunning(null);
|
||||
|
||||
if(formikSetFieldValueFunction)
|
||||
{
|
||||
//////////////////////////////////
|
||||
// reset field values in formik //
|
||||
//////////////////////////////////
|
||||
for (let key in qJobComplete.values)
|
||||
{
|
||||
formikSetFieldValueFunction(key, qJobComplete.values[key]);
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if the process step sent a new frontend-step-list, then refresh what we have in state (constructing new full model objects) //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
const updatedFrontendStepList = qJobComplete.updatedFrontendStepList;
|
||||
if(updatedFrontendStepList)
|
||||
{
|
||||
setSteps(updatedFrontendStepList);
|
||||
}
|
||||
|
||||
if (activeStep && activeStep.recordListFields)
|
||||
{
|
||||
setNeedRecords(true);
|
||||
@ -1385,89 +1463,98 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
>
|
||||
{({
|
||||
values, errors, touched, isSubmitting, setFieldValue,
|
||||
}) => (
|
||||
<Form style={formStyles} id={formId} autoComplete="off">
|
||||
<Card sx={mainCardStyles}>
|
||||
{
|
||||
!isWidget && (
|
||||
<Box mx={2} mt={-3}>
|
||||
<Stepper activeStep={activeStepIndex} alternativeLabel>
|
||||
{steps.map((step) => (
|
||||
<Step key={step.name}>
|
||||
<StepLabel>{step.label}</StepLabel>
|
||||
</Step>
|
||||
))}
|
||||
</Stepper>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
}) =>
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////
|
||||
// once we're in the formik form, use its setFieldValue function //
|
||||
// over top of the default one we created globally //
|
||||
///////////////////////////////////////////////////////////////////
|
||||
formikSetFieldValueFunction = setFieldValue;
|
||||
|
||||
<Box p={3}>
|
||||
<Box pb={isWidget ? 6 : "initial"}>
|
||||
{/***************************************************************************
|
||||
return (
|
||||
<Form style={formStyles} id={formId} autoComplete="off">
|
||||
<Card sx={mainCardStyles}>
|
||||
{
|
||||
!isWidget && (
|
||||
<Box mx={2} mt={-3} sx={{"& .MuiStepper-horizontal": {minHeight: "5rem"}}}>
|
||||
<Stepper activeStep={activeStepIndex} alternativeLabel>
|
||||
{steps.map((step) => (
|
||||
<Step key={step.name}>
|
||||
<StepLabel>{step.label}</StepLabel>
|
||||
</Step>
|
||||
))}
|
||||
</Stepper>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
<Box p={3}>
|
||||
<Box pb={isWidget ? 6 : "initial"}>
|
||||
{/***************************************************************************
|
||||
** step content - e.g., the appropriate form or other screen for the step **
|
||||
***************************************************************************/}
|
||||
{getDynamicStepContent(
|
||||
activeStepIndex,
|
||||
activeStep,
|
||||
{
|
||||
values,
|
||||
touched,
|
||||
formFields,
|
||||
errors,
|
||||
},
|
||||
processError,
|
||||
processValues,
|
||||
recordConfig,
|
||||
setFieldValue,
|
||||
)}
|
||||
{/********************************
|
||||
{getDynamicStepContent(
|
||||
activeStepIndex,
|
||||
activeStep,
|
||||
{
|
||||
values,
|
||||
touched,
|
||||
formFields,
|
||||
errors,
|
||||
},
|
||||
processError,
|
||||
processValues,
|
||||
recordConfig,
|
||||
setFieldValue,
|
||||
)}
|
||||
{/********************************
|
||||
** back &| next/submit buttons **
|
||||
********************************/}
|
||||
<Box mt={6} width="100%" display="flex" justifyContent="space-between" position={isWidget ? "absolute" : "initial"} bottom={isWidget ? "3rem" : "initial"} right={isWidget ? "1.5rem" : "initial"}>
|
||||
{true || activeStepIndex === 0 ? (
|
||||
<Box />
|
||||
) : (
|
||||
<MDButton variant="gradient" color="light" onClick={handleBack}>back</MDButton>
|
||||
)}
|
||||
{processError || qJobRunning || !activeStep ? (
|
||||
<Box />
|
||||
) : (
|
||||
<>
|
||||
{formError && (
|
||||
<MDTypography component="div" variant="caption" color="error" fontWeight="regular" align="right" fullWidth>
|
||||
{formError}
|
||||
</MDTypography>
|
||||
)}
|
||||
{
|
||||
noMoreSteps && <QCancelButton
|
||||
onClickHandler={handleCancelClicked}
|
||||
label={isModal ? "Close" : "Return"}
|
||||
iconName={isModal ? "cancel" : "arrow_back"}
|
||||
disabled={isSubmitting} />
|
||||
}
|
||||
{
|
||||
!noMoreSteps && (
|
||||
<Box component="div" py={3}>
|
||||
<Grid container justifyContent="flex-end" spacing={3}>
|
||||
{
|
||||
!isWidget && (
|
||||
<QCancelButton onClickHandler={handleCancelClicked} disabled={isSubmitting} />
|
||||
)
|
||||
}
|
||||
<QSubmitButton label={nextButtonLabel} iconName={nextButtonIcon} disabled={isSubmitting} />
|
||||
</Grid>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)}
|
||||
<Box mt={6} width="100%" display="flex" justifyContent="space-between" position={isWidget ? "absolute" : "initial"} bottom={isWidget ? "3rem" : "initial"} right={isWidget ? "1.5rem" : "initial"}>
|
||||
{true || activeStepIndex === 0 ? (
|
||||
<Box />
|
||||
) : (
|
||||
<MDButton variant="gradient" color="light" onClick={handleBack}>back</MDButton>
|
||||
)}
|
||||
{processError || qJobRunning || !activeStep ? (
|
||||
<Box />
|
||||
) : (
|
||||
<>
|
||||
{formError && (
|
||||
<MDTypography component="div" variant="caption" color="error" fontWeight="regular" align="right" fullWidth>
|
||||
{formError}
|
||||
</MDTypography>
|
||||
)}
|
||||
{
|
||||
noMoreSteps && <QCancelButton
|
||||
onClickHandler={handleCancelClicked}
|
||||
label={isModal ? "Close" : "Return"}
|
||||
iconName={isModal ? "cancel" : "arrow_back"}
|
||||
disabled={isSubmitting} />
|
||||
}
|
||||
{
|
||||
!noMoreSteps && (
|
||||
<Box component="div" py={3}>
|
||||
<Grid container justifyContent="flex-end" spacing={3}>
|
||||
{
|
||||
!isWidget && (
|
||||
<QCancelButton onClickHandler={handleCancelClicked} disabled={isSubmitting} />
|
||||
)
|
||||
}
|
||||
<QSubmitButton label={nextButtonLabel} iconName={nextButtonIcon} disabled={isSubmitting} />
|
||||
</Grid>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Card>
|
||||
</Form>
|
||||
)}
|
||||
</Card>
|
||||
</Form>
|
||||
)
|
||||
}}
|
||||
</Formik>
|
||||
);
|
||||
|
||||
|
@ -867,7 +867,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, initialQueryFilter, init
|
||||
for (let i = 0; i < queryFilter?.orderBys?.length; i++)
|
||||
{
|
||||
const fieldName = queryFilter.orderBys[i].fieldName;
|
||||
if (fieldName.indexOf(".") > -1)
|
||||
if (fieldName != null && fieldName.indexOf(".") > -1)
|
||||
{
|
||||
const joinTableName = fieldName.replaceAll(/\..*/g, "");
|
||||
if (!vjtToUse.has(joinTableName))
|
||||
|
@ -74,12 +74,14 @@ const qController = Client.getInstance();
|
||||
interface Props
|
||||
{
|
||||
table?: QTableMetaData;
|
||||
record?: QRecord;
|
||||
launchProcess?: QProcessMetaData;
|
||||
}
|
||||
|
||||
RecordView.defaultProps =
|
||||
{
|
||||
table: null,
|
||||
record: null,
|
||||
launchProcess: null,
|
||||
};
|
||||
|
||||
@ -127,10 +129,39 @@ export function renderSectionOfFields(key: string, fieldNames: string[], tableMe
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
export function getVisibleJoinTables(tableMetaData: QTableMetaData): Set<string>
|
||||
{
|
||||
const visibleJoinTables = new Set<string>();
|
||||
|
||||
for (let i = 0; i < tableMetaData?.sections.length; i++)
|
||||
{
|
||||
const section = tableMetaData?.sections[i];
|
||||
if (section.isHidden || !section.fieldNames || !section.fieldNames.length)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
section.fieldNames.forEach((fieldName) =>
|
||||
{
|
||||
const [field, tableForField] = TableUtils.getFieldAndTable(tableMetaData, fieldName);
|
||||
if (tableForField && tableForField.name != tableMetaData.name)
|
||||
{
|
||||
visibleJoinTables.add(tableForField.name);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (visibleJoinTables);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Record View Screen component.
|
||||
*******************************************************************************/
|
||||
function RecordView({table, launchProcess}: Props): JSX.Element
|
||||
function RecordView({table, record: overrideRecord, launchProcess}: Props): JSX.Element
|
||||
{
|
||||
const {id} = useParams();
|
||||
|
||||
@ -147,7 +178,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
||||
const [adornmentFieldsMap, setAdornmentFieldsMap] = useState(new Map<string, boolean>);
|
||||
const [deleteConfirmationOpen, setDeleteConfirmationOpen] = useState(false);
|
||||
const [metaData, setMetaData] = useState(null as QInstance);
|
||||
const [record, setRecord] = useState(null as QRecord);
|
||||
const [record, setRecord] = useState(overrideRecord ?? null as QRecord);
|
||||
const [tableSections, setTableSections] = useState([] as QTableSection[]);
|
||||
const [t1Section, setT1Section] = useState(null as QTableSection);
|
||||
const [t1SectionName, setT1SectionName] = useState(null as string);
|
||||
@ -381,31 +412,6 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
||||
reload();
|
||||
}, [location.pathname, location.hash]);
|
||||
|
||||
const getVisibleJoinTables = (tableMetaData: QTableMetaData): Set<string> =>
|
||||
{
|
||||
const visibleJoinTables = new Set<string>();
|
||||
|
||||
for (let i = 0; i < tableMetaData?.sections.length; i++)
|
||||
{
|
||||
const section = tableMetaData?.sections[i];
|
||||
if (section.isHidden || !section.fieldNames || !section.fieldNames.length)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
section.fieldNames.forEach((fieldName) =>
|
||||
{
|
||||
const [field, tableForField] = TableUtils.getFieldAndTable(tableMetaData, fieldName);
|
||||
if (tableForField && tableForField.name != tableMetaData.name)
|
||||
{
|
||||
visibleJoinTables.add(tableForField.name);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (visibleJoinTables);
|
||||
};
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** get an element (or empty) to use as help content for a section
|
||||
@ -481,7 +487,18 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
||||
let record: QRecord;
|
||||
try
|
||||
{
|
||||
record = await qController.get(tableName, id, tableVariant, null, queryJoins);
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// if the component took in a record object, then we don't need to GET it //
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
if(overrideRecord)
|
||||
{
|
||||
record = overrideRecord;
|
||||
}
|
||||
else
|
||||
{
|
||||
record = await qController.get(tableName, id, tableVariant, null, queryJoins);
|
||||
}
|
||||
|
||||
setRecord(record);
|
||||
recordAnalytics({category: "tableEvents", action: "view", label: tableMetaData?.label + " / " + record?.recordLabel});
|
||||
}
|
||||
|
163
src/qqq/pages/records/view/RecordViewByUniqueKey.tsx
Normal file
163
src/qqq/pages/records/view/RecordViewByUniqueKey.tsx
Normal file
@ -0,0 +1,163 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. 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 {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
||||
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
||||
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 {QueryJoin} from "@kingsrook/qqq-frontend-core/lib/model/query/QueryJoin";
|
||||
import {Alert, Box} from "@mui/material";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import BaseLayout from "qqq/layouts/BaseLayout";
|
||||
import RecordView, {getVisibleJoinTables} from "qqq/pages/records/view/RecordView";
|
||||
import Client from "qqq/utils/qqq/Client";
|
||||
import TableUtils from "qqq/utils/qqq/TableUtils";
|
||||
import React, {useEffect, useState} from "react";
|
||||
import {useSearchParams} from "react-router-dom";
|
||||
|
||||
interface RecordViewByUniqueKeyProps
|
||||
{
|
||||
table: QTableMetaData;
|
||||
}
|
||||
|
||||
RecordViewByUniqueKey.defaultProps = {};
|
||||
|
||||
const qController = Client.getInstance();
|
||||
|
||||
/***************************************************************************
|
||||
** Wrapper around RecordView, that reads a unique key from the query string,
|
||||
** looks for a record matching that key, and shows that record.
|
||||
***************************************************************************/
|
||||
export default function RecordViewByUniqueKey({table}: RecordViewByUniqueKeyProps): JSX.Element
|
||||
{
|
||||
const tableName = table.name;
|
||||
|
||||
const [asyncLoadInited, setAsyncLoadInited] = useState(false);
|
||||
const [tableMetaData, setTableMetaData] = useState(null as QTableMetaData);
|
||||
const [doneLoading, setDoneLoading] = useState(false);
|
||||
const [record, setRecord] = useState(null as QRecord);
|
||||
const [errorMessage, setErrorMessage] = useState(null as string);
|
||||
|
||||
const [queryParams] = useSearchParams();
|
||||
|
||||
if (!asyncLoadInited)
|
||||
{
|
||||
setAsyncLoadInited(true);
|
||||
|
||||
(async () =>
|
||||
{
|
||||
const tableMetaData = await qController.loadTableMetaData(tableName);
|
||||
setTableMetaData(tableMetaData);
|
||||
|
||||
const criteria: QFilterCriteria[] = [];
|
||||
for (let [name, value] of queryParams.entries())
|
||||
{
|
||||
criteria.push(new QFilterCriteria(name, QCriteriaOperator.EQUALS, [value]));
|
||||
if(!tableMetaData.fields.has(name))
|
||||
{
|
||||
setErrorMessage(`Query-string parameter [${name}] is not a defined field on the ${tableMetaData.label} table.`);
|
||||
setDoneLoading(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let queryJoins: QueryJoin[] = null;
|
||||
const visibleJoinTables = getVisibleJoinTables(tableMetaData);
|
||||
if (visibleJoinTables.size > 0)
|
||||
{
|
||||
queryJoins = TableUtils.getQueryJoins(tableMetaData, visibleJoinTables);
|
||||
}
|
||||
|
||||
const filter = new QQueryFilter(criteria, null, null, "AND", 0, 2);
|
||||
qController.query(tableName, filter, queryJoins)
|
||||
.then((queryResult) =>
|
||||
{
|
||||
setDoneLoading(true);
|
||||
if (queryResult.length == 1)
|
||||
{
|
||||
setRecord(queryResult[0]);
|
||||
}
|
||||
else if (queryResult.length == 0)
|
||||
{
|
||||
setErrorMessage(`No ${tableMetaData.label} record was found matching the given values.`);
|
||||
}
|
||||
else if (queryResult.length > 1)
|
||||
{
|
||||
setErrorMessage(`More than one ${tableMetaData.label} record was found matching the given values.`);
|
||||
}
|
||||
})
|
||||
.catch((error) =>
|
||||
{
|
||||
setDoneLoading(true);
|
||||
console.log(error);
|
||||
if (error && error.message)
|
||||
{
|
||||
setErrorMessage(error.message);
|
||||
}
|
||||
else if (error && error.response && error.response.data && error.response.data.error)
|
||||
{
|
||||
setErrorMessage(error.response.data.error);
|
||||
}
|
||||
else
|
||||
{
|
||||
setErrorMessage("Unexpected error running query");
|
||||
}
|
||||
});
|
||||
})();
|
||||
}
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if (asyncLoadInited)
|
||||
{
|
||||
setAsyncLoadInited(false);
|
||||
setDoneLoading(false);
|
||||
setRecord(null);
|
||||
}
|
||||
}, [queryParams]);
|
||||
|
||||
if (!doneLoading)
|
||||
{
|
||||
return (<div>Loading...</div>);
|
||||
}
|
||||
else if (record)
|
||||
{
|
||||
return (<RecordView table={table} record={record} />);
|
||||
}
|
||||
else if (errorMessage)
|
||||
{
|
||||
return (<BaseLayout>
|
||||
<Box className="recordView">
|
||||
<Grid container>
|
||||
<Grid item xs={12}>
|
||||
<Box mb={3}>
|
||||
{
|
||||
<Alert color="error" sx={{mb: 3}}>{errorMessage}</Alert>
|
||||
}
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
</BaseLayout>);
|
||||
}
|
||||
}
|
@ -133,6 +133,11 @@ class TableUtils
|
||||
*******************************************************************************/
|
||||
public static getFieldAndTable(tableMetaData: QTableMetaData, fieldName: string): [QFieldMetaData, QTableMetaData]
|
||||
{
|
||||
if(!fieldName)
|
||||
{
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
if (fieldName.indexOf(".") > -1)
|
||||
{
|
||||
const nameParts = fieldName.split(".", 2);
|
||||
|
@ -335,7 +335,7 @@ public class QSeleniumLib
|
||||
return;
|
||||
}
|
||||
|
||||
if(elements.stream().noneMatch(e -> e.getText().toLowerCase().contains(textContains)))
|
||||
if(elements.stream().noneMatch(e -> e.getText().toLowerCase().contains(textContains.toLowerCase())))
|
||||
{
|
||||
LOG.debug("Found non-existence of element(s) matching selector [" + cssSelector + "] containing text [" + textContains + "]");
|
||||
return;
|
||||
@ -345,7 +345,7 @@ public class QSeleniumLib
|
||||
}
|
||||
while(start + (1000 * WAIT_SECONDS) > System.currentTimeMillis());
|
||||
|
||||
fail("Failed for non-existence of element matching selector [" + cssSelector + "] after [" + WAIT_SECONDS + "] seconds.");
|
||||
fail("Failed for non-existence of element matching selector [" + cssSelector + "] containing text [" + textContains + "] after [" + WAIT_SECONDS + "] seconds.");
|
||||
}
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user