Compare commits

..

32 Commits

Author SHA1 Message Date
0831a87674 CE-1180 Add joins to request for the record 2024-05-13 08:46:46 -05:00
dd5cd459ce CE-1180 reset formik form values (to latest values from backend) after each backend step 2024-05-13 08:46:11 -05:00
c200cc9fab CE-1180 Add a null-check in getFieldandTable 2024-05-10 15:51:54 -05:00
17f378131d CE-1180 Add a null-check in ensureOrderBysFromJoinTablesAreVisibleTables (not sure why, but feels safe and good) 2024-05-10 15:51:37 -05:00
376a7a342e do toLowerCase on both sides of a contains check... also better fail message for not-contains text. 2024-05-10 15:50:29 -05:00
fcadea3192 CE-1180 Better support for processes w/ dynamic flows, by using updtedFrontnedStepList;
- fixed min-height on the bar w/ steps (e.g., before they're known)
- updated EDIT_FORM to support values: "includeFieldNames" - to do a sub-set of field names (so you can organize them a bit across multiple EDIT_FORM's) and "sectionLabel"
2024-05-10 15:49:39 -05:00
086ab775fc CE-1180 Update qqq-frontend-core to 1.0.100 💯 🎉 - updatedFrontendStepList in process output 2024-05-10 15:46:43 -05:00
5693661d20 CE-1180 Updated to support multi-value keys. also, support tables (e.g., api tables) w/o a primary key, by redirecting a successful UK lookup to the (new) /key?queryString record-view screen 2024-05-10 15:45:44 -05:00
8c9224aceb CE-1180 Add new table/key?queryString endpoint, to look up a record by a (unique) key and then view it 2024-05-10 15:33:56 -05:00
1859dd603d Merge pull request #58 from Kingsrook/integration/sprint-41
Integration/sprint 41
2024-05-02 08:38:09 -05:00
74f8f11737 Merge pull request #57 from Kingsrook/feature/CE-1068-add-basic-functionality-of
Feature/ce 1068 add basic functionality of
2024-05-02 08:36:30 -05:00
0629172270 Merged feature/CE-1068-add-basic-functionality-of into integration/sprint-41 2024-05-01 16:59:24 -05:00
1bf1f09e9d CE-1068 - scroll-top-top to show alerts in modals 2024-05-01 15:38:14 -05:00
e0f689544d CE-1068: fixed bug around filter variables on possible value 2024-05-01 13:43:41 -05:00
f3d08ef683 Merged feature/CE-882-add-functionality-of-sharing into feature/CE-1068-add-basic-functionality-of 2024-05-01 10:54:38 -05:00
1aff749f72 CE-1068: made width for date times a little wider 2024-05-01 10:54:33 -05:00
ccc622e0e9 Merge branch 'feature/CE-1068-add-basic-functionality-of' into integration/sprint-41 2024-05-01 10:21:54 -05:00
a6662eeb07 CE-1068: bug fixes 2024-05-01 10:04:20 -05:00
c8b673fb46 Merged feature/CE-1068-add-basic-functionality-of into integration/sprint-41 2024-04-30 19:51:10 -05:00
f19e36a6bf CE-882 - Better avoidance of savedView under a table query screen 2024-04-30 19:42:48 -05:00
c708ec3b9a CE-882 - Better handling of stupidly long titles 2024-04-30 19:42:34 -05:00
7e40fa90e9 CE-1068: updates to fix selenium tests 2024-04-30 19:28:35 -05:00
680d185eb5 Merged feature/CE-1068-add-basic-functionality-of into integration/sprint-41 2024-04-30 19:21:02 -05:00
4f37488d37 Merged feature/CE-882-add-functionality-of-sharing into integration/sprint-41 2024-04-30 19:20:50 -05:00
d20700edb1 CE-882 - Turn off 'scope' for time being 2024-04-30 19:20:22 -05:00
d17c7f6990 CE-1068: more updates for other operators to support variables 2024-04-30 19:04:34 -05:00
0d7849b7dc Merged feature/CE-882-add-functionality-of-sharing into integration/sprint-41 2024-04-30 16:42:04 -05:00
57098b5f05 CE-882 - Add awareness of shared views 2024-04-30 15:28:22 -05:00
7316b6141b CE-1068 - possible-values working in dynamic form (i think!) 2024-04-30 14:42:20 -05:00
8bc2479716 CE-1068: added passing of allowVariables to date criteria 2024-04-30 14:04:38 -05:00
010f80def3 Merged feature/CE-1068-add-basic-functionality-of into integration/sprint-41 2024-04-30 11:45:38 -05:00
38b8f47409 Merged feature/CE-882-add-functionality-of-sharing into integration/sprint-41 2024-04-29 10:41:29 -05:00
25 changed files with 906 additions and 376 deletions

View File

@ -6,7 +6,7 @@
"@auth0/auth0-react": "1.10.2", "@auth0/auth0-react": "1.10.2",
"@emotion/react": "11.7.1", "@emotion/react": "11.7.1",
"@emotion/styled": "11.6.0", "@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/icons-material": "5.4.1",
"@mui/material": "5.11.1", "@mui/material": "5.11.1",
"@mui/styles": "5.11.1", "@mui/styles": "5.11.1",

View File

@ -49,6 +49,7 @@ import EntityEdit from "qqq/pages/records/edit/RecordEdit";
import RecordQuery from "qqq/pages/records/query/RecordQuery"; import RecordQuery from "qqq/pages/records/query/RecordQuery";
import RecordDeveloperView from "qqq/pages/records/view/RecordDeveloperView"; import RecordDeveloperView from "qqq/pages/records/view/RecordDeveloperView";
import RecordView from "qqq/pages/records/view/RecordView"; import RecordView from "qqq/pages/records/view/RecordView";
import RecordViewByUniqueKey from "qqq/pages/records/view/RecordViewByUniqueKey";
import GoogleAnalyticsUtils, {AnalyticsModel} from "qqq/utils/GoogleAnalyticsUtils"; import GoogleAnalyticsUtils, {AnalyticsModel} from "qqq/utils/GoogleAnalyticsUtils";
import Client from "qqq/utils/qqq/Client"; import Client from "qqq/utils/qqq/Client";
import ProcessUtils from "qqq/utils/qqq/ProcessUtils"; import ProcessUtils from "qqq/utils/qqq/ProcessUtils";
@ -392,6 +393,13 @@ export default function App()
component: <RecordView table={table} />, component: <RecordView table={table} />,
}); });
routeList.push({
name: `${app.label} View`,
key: `${app.name}.view`,
route: `${path}/key`,
component: <RecordViewByUniqueKey table={table} />,
});
routeList.push({ routeList.push({
name: `${app.label}`, name: `${app.label}`,
key: `${app.name}.edit`, key: `${app.name}.edit`,

View File

@ -174,7 +174,8 @@ function QDynamicForm({formData, formLabel, bulkEditMode, bulkEditSwitchChangeHa
<DynamicSelect <DynamicSelect
tableName={field.possibleValueProps.tableName} tableName={field.possibleValueProps.tableName}
processName={field.possibleValueProps.processName} processName={field.possibleValueProps.processName}
fieldName={fieldName} possibleValueSourceName={field.possibleValueProps.possibleValueSourceName}
fieldName={field.possibleValueProps.fieldName}
isEditable={field.isEditable} isEditable={field.isEditable}
fieldLabel="" fieldLabel=""
initialValue={values[fieldName]} initialValue={values[fieldName]}

View File

@ -172,6 +172,7 @@ class DynamicFormUtils
{ {
isPossibleValue: true, isPossibleValue: true,
tableName: tableName, tableName: tableName,
fieldName: field.name,
initialDisplayValue: initialDisplayValue, initialDisplayValue: initialDisplayValue,
}; };
} }
@ -181,6 +182,7 @@ class DynamicFormUtils
{ {
isPossibleValue: true, isPossibleValue: true,
processName: processName, processName: processName,
fieldName: field.name,
initialDisplayValue: initialDisplayValue, initialDisplayValue: initialDisplayValue,
}; };
} }
@ -190,6 +192,8 @@ class DynamicFormUtils
{ {
isPossibleValue: true, isPossibleValue: true,
initialDisplayValue: initialDisplayValue, initialDisplayValue: initialDisplayValue,
fieldName: field.name,
possibleValueSourceName: field.possibleValueSourceName
}; };
} }
} }

View File

@ -124,19 +124,15 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
{ {
console.log("DynamicSelect - if you provide a processName, you must also provide a fieldName"); console.log("DynamicSelect - if you provide a processName, you must also provide a fieldName");
} }
if(fieldName && possibleValueSourceName)
{
console.log("DynamicSelect - if you provide a fieldName and a possibleValueSourceName, the possibleValueSourceName will be ignored");
}
if(!fieldName && !possibleValueSourceName) if(!fieldName && !possibleValueSourceName)
{ {
console.log("DynamicSelect - you must provide either a fieldName (and a tableName or processName) or a possibleValueSourceName"); console.log("DynamicSelect - you must provide either a fieldName (and a tableName or processName) or a possibleValueSourceName");
} }
if(fieldName) if(fieldName && !possibleValueSourceName)
{ {
if(!tableName || !processName) if(!tableName || !processName)
{ {
console.log("DynamicSelect - if you provide a fieldName, you must also provide a tableName or processName"); console.log("DynamicSelect - if you provide a fieldName and not a possibleValueSourceName, then you must also provide a tableName or processName");
} }
} }
if(possibleValueSourceName) if(possibleValueSourceName)
@ -198,7 +194,7 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
(async () => (async () =>
{ {
// console.log(`doing a search with ${searchTerm}`); // console.log(`doing a search with ${searchTerm}`);
const results: QPossibleValue[] = await qController.possibleValues(tableName, processName, fieldName ?? possibleValueSourceName, searchTerm ?? "", null, otherValues); const results: QPossibleValue[] = await qController.possibleValues(tableName, processName, possibleValueSourceName ?? fieldName, searchTerm ?? "", null, otherValues);
if(tableMetaData == null && tableName) if(tableMetaData == null && tableName)
{ {
@ -231,7 +227,7 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
setLoading(true); setLoading(true);
setOptions([]); setOptions([]);
console.log("Refreshing possible values..."); console.log("Refreshing possible values...");
const results: QPossibleValue[] = await qController.possibleValues(tableName, processName, fieldName ?? possibleValueSourceName, searchTerm ?? "", null, otherValues); const results: QPossibleValue[] = await qController.possibleValues(tableName, processName, possibleValueSourceName ?? fieldName, searchTerm ?? "", null, otherValues);
setLoading(false); setLoading(false);
setOptions([ ...results ]); setOptions([ ...results ]);
setOtherValuesWhenResultsWereLoaded(JSON.stringify(Object.fromEntries(otherValues))); setOtherValuesWhenResultsWereLoaded(JSON.stringify(Object.fromEntries(otherValues)));

View File

@ -927,7 +927,7 @@ function EntityForm(props: Props): JSX.Element
else else
{ {
setAlertContent(error.message); setAlertContent(error.message);
HtmlUtils.autoScroll(0); scrollToTopToShowAlert();
} }
}); });
} }
@ -973,7 +973,7 @@ function EntityForm(props: Props): JSX.Element
else else
{ {
setAlertContent(error.message); setAlertContent(error.message);
HtmlUtils.autoScroll(0); scrollToTopToShowAlert();
} }
}); });
} }
@ -981,6 +981,22 @@ function EntityForm(props: Props): JSX.Element
}; };
/*******************************************************************************
**
*******************************************************************************/
function scrollToTopToShowAlert()
{
if (props.isModal)
{
document.getElementById("modalTopReference")?.scrollIntoView();
}
else
{
HtmlUtils.autoScroll(0);
}
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
@ -1265,6 +1281,7 @@ function EntityForm(props: Props): JSX.Element
return ( return (
<Box sx={{position: "absolute", overflowY: "auto", maxHeight: "100%", width: "100%"}}> <Box sx={{position: "absolute", overflowY: "auto", maxHeight: "100%", width: "100%"}}>
<Card sx={{my: 5, mx: "auto", p: 6, pb: 0, maxWidth: "1024px"}}> <Card sx={{my: 5, mx: "auto", p: 6, pb: 0, maxWidth: "1024px"}}>
<span id="modalTopReference"></span>
{body} {body}
</Card> </Card>
</Box> </Box>

View File

@ -94,16 +94,19 @@ function QBreadcrumbs({icon, title, route, light}: Props): JSX.Element
{ {
//////////////////////////////////////////////////////// ////////////////////////////////////////////////////////
// avoid showing "saved view" as a breadcrumb element // // avoid showing "saved view" as a breadcrumb element //
// e.g., if at /app/table/savedView/1 (so where i==2) //
//////////////////////////////////////////////////////// ////////////////////////////////////////////////////////
if(routes[i] === "savedView") if(routes[i] === "savedView" && i == 2)
{ {
continue; continue;
} }
/////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////
// avoid showing the table name if it's the element before savedView // // avoid showing the table name if it's the element before savedView //
// e.g., when at /app/table/savedView/1 (so where i==1) //
// we want to just be showing "App" //
/////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////
if(i < routes.length - 1 && routes[i+1] == "savedView") if(i < routes.length - 1 && routes[i+1] == "savedView" && i == 1)
{ {
continue; continue;
} }

View File

@ -35,6 +35,7 @@ import DialogTitle from "@mui/material/DialogTitle";
import Grid from "@mui/material/Grid"; import Grid from "@mui/material/Grid";
import Icon from "@mui/material/Icon"; import Icon from "@mui/material/Icon";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import {any} from "prop-types";
import React, {useState} from "react"; import React, {useState} from "react";
import {useNavigate} from "react-router-dom"; import {useNavigate} from "react-router-dom";
import {QCancelButton} from "qqq/components/buttons/DefaultButtons"; import {QCancelButton} from "qqq/components/buttons/DefaultButtons";
@ -71,7 +72,12 @@ function hasGotoFieldNames(tableMetaData: QTableMetaData): boolean
function GotoRecordDialog(props: Props): JSX.Element 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 pkey = props?.tableMetaData?.fields.get(props?.tableMetaData?.primaryKeyField);
let addedPkey = false; let addedPkey = false;
@ -82,31 +88,38 @@ function GotoRecordDialog(props: Props): JSX.Element
{ {
for (let i = 0; i < mdbMetaData.gotoFieldNames.length; i++) for (let i = 0; i < mdbMetaData.gotoFieldNames.length; i++)
{ {
// todo - multi-field keys!! const option: QFieldMetaData[] = [];
let fieldName = mdbMetaData.gotoFieldNames[i][0]; options.push(option);
let field = props.tableMetaData.fields.get(fieldName); for (let j = 0; j < mdbMetaData.gotoFieldNames[i].length; j++)
if (field)
{ {
fields.push(field); let fieldName = mdbMetaData.gotoFieldNames[i][j];
let field = props.tableMetaData.fields.get(fieldName);
if (field.name == pkey.name) 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) if (pkey && !addedPkey)
{ {
fields.unshift(pkey); options.unshift([pkey]);
} }
const makeInitialValues = () => const makeInitialValues = () =>
{ {
const rs = {} as { [field: string]: string }; const rs = {} as { [field: string]: string };
fields.forEach((field) => rs[field.name] = ""); options.forEach((option) => option.forEach((field) => rs[field.name] = ""));
return (rs); return (rs);
}; };
@ -141,11 +154,16 @@ function GotoRecordDialog(props: Props): JSX.Element
} }
else if (e.key == "Enter" && targetId?.startsWith("gotoInput-")) 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(); document.getElementById("gotoButton-" + index).click();
} }
}; };
/***************************************************************************
** event handler for close button
***************************************************************************/
const closeRequested = () => const closeRequested = () =>
{ {
if (props.mayClose) 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(""); 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 try
{ {
const queryResult = await qController.query(props.tableMetaData.name, filter, null, props.tableVariant); 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) 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(); close();
} }
else else
{ {
setError("More than 1 record found..."); setError("More than 1 record was found...");
setTimeout(() => setError(""), 3000); setTimeout(() => setError(""), 3000);
} }
} }
@ -187,7 +256,7 @@ function GotoRecordDialog(props: Props): JSX.Element
if (props.tableMetaData) if (props.tableMetaData)
{ {
if (fields.length == 0 && !error) if (options.length == 0 && !error)
{ {
setError("This table is not configured for this feature."); setError("This table is not configured for this feature.");
} }
@ -200,31 +269,38 @@ function GotoRecordDialog(props: Props): JSX.Element
<DialogContent> <DialogContent>
{props.subHeader} {props.subHeader}
{ {
fields.map((field, index) => options.map((option, optionIndex) =>
( <Box key={optionIndex}>
<Grid key={field.name} container alignItems="center" py={1}> {
<Grid item xs={3} textAlign="right" pr={2}> option.map((field, index) =>
{field.label} (
</Grid> <Grid key={field.name} container alignItems="center" py={1}>
<Grid item xs={6}> <Grid item xs={3} textAlign="right" pr={2}>
<TextField {field.label}
id={`gotoInput-${index}`} </Grid>
autoFocus={index == 0} <Grid item xs={6}>
autoComplete="off" <TextField
inputProps={{width: "100%"}} id={`gotoInput-${optionIndex}-${index}`}
onChange={(e) => handleChange(field.name, e.target.value)} autoFocus={optionIndex == 0 && index == 0}
value={values[field.name]} autoComplete="off"
sx={{width: "100%"}} inputProps={{width: "100%"}}
onFocus={event => event.target.select()} onChange={(e) => handleChange(field.name, e.target.value)}
/> value={values[field.name]}
</Grid> sx={{width: "100%"}}
<Grid item xs={1} pl={2}> onFocus={event => event.target.select()}
<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 </Grid>
</MDButton> <Grid item xs={1} pl={2}>
</Grid> {
</Grid> (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 && error &&

View File

@ -25,8 +25,7 @@ import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QT
import {QJobComplete} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobComplete"; import {QJobComplete} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobComplete";
import {QJobError} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobError"; import {QJobError} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobError";
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord"; import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
import {Alert, Button} from "@mui/material"; import {Alert, Box, Button} from "@mui/material";
import Box from "@mui/material/Box";
import Dialog from "@mui/material/Dialog"; import Dialog from "@mui/material/Dialog";
import DialogActions from "@mui/material/DialogActions"; import DialogActions from "@mui/material/DialogActions";
import DialogContent from "@mui/material/DialogContent"; import DialogContent from "@mui/material/DialogContent";
@ -60,7 +59,7 @@ interface Props
view?: RecordQueryView; view?: RecordQueryView;
viewAsJson?: string; viewAsJson?: string;
viewOnChangeCallback?: (selectedSavedViewId: number) => void; viewOnChangeCallback?: (selectedSavedViewId: number) => void;
loadingSavedView: boolean loadingSavedView: boolean;
queryScreenUsage: QueryScreenUsage; queryScreenUsage: QueryScreenUsage;
} }
@ -69,6 +68,8 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
const navigate = useNavigate(); const navigate = useNavigate();
const [savedViews, setSavedViews] = useState([] as QRecord[]); const [savedViews, setSavedViews] = useState([] as QRecord[]);
const [yourSavedViews, setYourSavedViews] = useState([] as QRecord[]);
const [viewsSharedWithYou, setViewsSharedWithYou] = useState([] as QRecord[]);
const [savedViewsMenu, setSavedViewsMenu] = useState(null); const [savedViewsMenu, setSavedViewsMenu] = useState(null);
const [savedViewsHaveLoaded, setSavedViewsHaveLoaded] = useState(false); const [savedViewsHaveLoaded, setSavedViewsHaveLoaded] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
@ -91,7 +92,7 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
const CLEAR_OPTION = "New View"; const CLEAR_OPTION = "New View";
const NEW_REPORT_OPTION = "Create Report from Current View"; const NEW_REPORT_OPTION = "Create Report from Current View";
const {accentColor, accentColorLight} = useContext(QContext); const {accentColor, accentColorLight, userId: currentUserId} = useContext(QContext);
///////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////
// this component is used by <RecordQuery> - but that component has different usages - // // this component is used by <RecordQuery> - but that component has different usages - //
@ -114,13 +115,13 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
{ {
setSavedViewsHaveLoaded(true); setSavedViewsHaveLoaded(true);
}); });
}, [location, tableMetaData]) }, [location, tableMetaData]);
const baseView = currentSavedView ? JSON.parse(currentSavedView.values.get("viewJson")) as RecordQueryView : tableDefaultView; const baseView = currentSavedView ? JSON.parse(currentSavedView.values.get("viewJson")) as RecordQueryView : tableDefaultView;
const viewDiffs = SavedViewUtils.diffViews(tableMetaData, baseView, view); const viewDiffs = SavedViewUtils.diffViews(tableMetaData, baseView, view);
let viewIsModified = false; let viewIsModified = false;
if(viewDiffs.length > 0) if (viewDiffs.length > 0)
{ {
viewIsModified = true; viewIsModified = true;
} }
@ -130,7 +131,7 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
*******************************************************************************/ *******************************************************************************/
async function loadSavedViews() async function loadSavedViews()
{ {
if (! tableMetaData) if (!tableMetaData)
{ {
return; return;
} }
@ -140,8 +141,24 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
let savedViews = await makeSavedViewRequest("querySavedView", formData); let savedViews = await makeSavedViewRequest("querySavedView", formData);
setSavedViews(savedViews); setSavedViews(savedViews);
}
const yourSavedViews: QRecord[] = [];
const viewsSharedWithYou: QRecord[] = [];
for (let i = 0; i < savedViews.length; i++)
{
const record = savedViews[i];
if (record.values.get("userId") == currentUserId)
{
yourSavedViews.push(record);
}
else
{
viewsSharedWithYou.push(record);
}
}
setYourSavedViews(yourSavedViews);
setViewsSharedWithYou(viewsSharedWithYou);
}
/******************************************************************************* /*******************************************************************************
@ -152,14 +169,13 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
setSaveFilterPopupOpen(false); setSaveFilterPopupOpen(false);
closeSavedViewsMenu(); closeSavedViewsMenu();
viewOnChangeCallback(record.values.get("id")); viewOnChangeCallback(record.values.get("id"));
if(isQueryScreen) if (isQueryScreen)
{ {
navigate(`${metaData.getTablePathByName(tableMetaData.name)}/savedView/${record.values.get("id")}`); navigate(`${metaData.getTablePathByName(tableMetaData.name)}/savedView/${record.values.get("id")}`);
} }
}; };
/******************************************************************************* /*******************************************************************************
** fired when a save option is selected from the save... button/dropdown combo ** fired when a save option is selected from the save... button/dropdown combo
*******************************************************************************/ *******************************************************************************/
@ -171,12 +187,12 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
setSaveFilterPopupOpen(true); setSaveFilterPopupOpen(true);
setIsSaveFilterAs(false); setIsSaveFilterAs(false);
setIsRenameFilter(false); setIsRenameFilter(false);
setIsDeleteFilter(false) setIsDeleteFilter(false);
switch(optionName) switch (optionName)
{ {
case SAVE_OPTION: case SAVE_OPTION:
if(currentSavedView == null) if (currentSavedView == null)
{ {
setSavedViewNameInputValue(""); setSavedViewNameInputValue("");
} }
@ -186,28 +202,28 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
setIsSaveFilterAs(true); setIsSaveFilterAs(true);
break; break;
case CLEAR_OPTION: case CLEAR_OPTION:
setSaveFilterPopupOpen(false) setSaveFilterPopupOpen(false);
viewOnChangeCallback(null); viewOnChangeCallback(null);
if(isQueryScreen) if (isQueryScreen)
{ {
navigate(metaData.getTablePathByName(tableMetaData.name)); navigate(metaData.getTablePathByName(tableMetaData.name));
} }
break; break;
case RENAME_OPTION: case RENAME_OPTION:
if(currentSavedView != null) if (currentSavedView != null)
{ {
setSavedViewNameInputValue(currentSavedView.values.get("label")); setSavedViewNameInputValue(currentSavedView.values.get("label"));
} }
setIsRenameFilter(true); setIsRenameFilter(true);
break; break;
case DELETE_OPTION: case DELETE_OPTION:
setIsDeleteFilter(true) setIsDeleteFilter(true);
break; break;
case NEW_REPORT_OPTION: case NEW_REPORT_OPTION:
createNewReport(); createNewReport();
break; break;
} }
} };
/******************************************************************************* /*******************************************************************************
@ -215,11 +231,11 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
*******************************************************************************/ *******************************************************************************/
function createNewReport() function createNewReport()
{ {
const defaultValues: {[key: string]: any} = {}; const defaultValues: { [key: string]: any } = {};
defaultValues.tableName = tableMetaData.name; defaultValues.tableName = tableMetaData.name;
let filterForBackend = JSON.parse(JSON.stringify(view.queryFilter)); let filterForBackend = JSON.parse(JSON.stringify(view.queryFilter));
filterForBackend = FilterUtils.prepQueryFilterForBackend(tableMetaData, filterForBackend); filterForBackend = FilterUtils.prepQueryFilterForBackend(tableMetaData, filterForBackend);
defaultValues.queryFilterJson = JSON.stringify(filterForBackend); defaultValues.queryFilterJson = JSON.stringify(filterForBackend);
defaultValues.columnsJson = JSON.stringify(view.queryColumns); defaultValues.columnsJson = JSON.stringify(view.queryColumns);
@ -227,7 +243,6 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
} }
/******************************************************************************* /*******************************************************************************
** fired when save or delete button saved on confirmation dialogs ** fired when save or delete button saved on confirmation dialogs
*******************************************************************************/ *******************************************************************************/
@ -247,7 +262,7 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
setSaveFilterPopupOpen(false); setSaveFilterPopupOpen(false);
setSaveOptionsOpen(false); setSaveOptionsOpen(false);
await(async() => await (async () =>
{ {
handleDropdownOptionClick(CLEAR_OPTION); handleDropdownOptionClick(CLEAR_OPTION);
})(); })();
@ -267,14 +282,14 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
// strip away incomplete filters too, just for cleaner saved view filters // // strip away incomplete filters too, just for cleaner saved view filters //
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
FilterUtils.stripAwayIncompleteCriteria(viewObject.queryFilter) FilterUtils.stripAwayIncompleteCriteria(viewObject.queryFilter);
formData.append("viewJson", JSON.stringify(viewObject)); formData.append("viewJson", JSON.stringify(viewObject));
if (isSaveFilterAs || isRenameFilter || currentSavedView == null) if (isSaveFilterAs || isRenameFilter || currentSavedView == null)
{ {
formData.append("label", savedViewNameInputValue); formData.append("label", savedViewNameInputValue);
if(currentSavedView != null && isRenameFilter) if (currentSavedView != null && isRenameFilter)
{ {
formData.append("id", currentSavedView.values.get("id")); formData.append("id", currentSavedView.values.get("id"));
} }
@ -285,7 +300,7 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
formData.append("label", currentSavedView?.values.get("label")); formData.append("label", currentSavedView?.values.get("label"));
} }
const recordList = await makeSavedViewRequest("storeSavedView", formData); const recordList = await makeSavedViewRequest("storeSavedView", formData);
await(async() => await (async () =>
{ {
if (recordList && recordList.length > 0) if (recordList && recordList.length > 0)
{ {
@ -302,11 +317,11 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
catch (e: any) catch (e: any)
{ {
let message = JSON.stringify(e); let message = JSON.stringify(e);
if(typeof e == "string") if (typeof e == "string")
{ {
message = e; message = e;
} }
else if(typeof e == "object" && e.message) else if (typeof e == "object" && e.message)
{ {
message = e.message; message = e.message;
} }
@ -321,7 +336,6 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
} }
/******************************************************************************* /*******************************************************************************
** hides/shows the save options ** hides/shows the save options
*******************************************************************************/ *******************************************************************************/
@ -331,7 +345,6 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
}; };
/******************************************************************************* /*******************************************************************************
** closes save options menu (on clickaway) ** closes save options menu (on clickaway)
*******************************************************************************/ *******************************************************************************/
@ -346,7 +359,6 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
}; };
/******************************************************************************* /*******************************************************************************
** stores the current dialog input text to state ** stores the current dialog input text to state
*******************************************************************************/ *******************************************************************************/
@ -356,7 +368,6 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
}; };
/******************************************************************************* /*******************************************************************************
** closes current dialog ** closes current dialog
*******************************************************************************/ *******************************************************************************/
@ -366,7 +377,6 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
}; };
/******************************************************************************* /*******************************************************************************
** make a request to the backend for various savedView processes ** make a request to the backend for various savedView processes
*******************************************************************************/ *******************************************************************************/
@ -375,7 +385,7 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
///////////////////////// /////////////////////////
// fetch saved filters // // fetch saved filters //
///////////////////////// /////////////////////////
let savedViews = [] as QRecord[] let savedViews = [] as QRecord[];
try try
{ {
////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////
@ -386,12 +396,12 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
if (processResult instanceof QJobError) if (processResult instanceof QJobError)
{ {
const jobError = processResult as QJobError; const jobError = processResult as QJobError;
throw(jobError.error); throw (jobError.error);
} }
else else
{ {
const result = processResult as QJobComplete; const result = processResult as QJobComplete;
if(result.values.savedViewList) if (result.values.savedViewList)
{ {
for (let i = 0; i < result.values.savedViewList.length; i++) for (let i = 0; i < result.values.savedViewList.length; i++)
{ {
@ -403,7 +413,7 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
} }
catch (e) catch (e)
{ {
throw(e); throw (e);
} }
return (savedViews); return (savedViews);
@ -416,17 +426,27 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
const tooltipMaxWidth = (maxWidth: string) => const tooltipMaxWidth = (maxWidth: string) =>
{ {
return ({slotProps: { return ({
tooltip: { slotProps: {
sx: { tooltip: {
maxWidth: maxWidth sx: {
maxWidth: maxWidth
}
} }
} }
}}) });
} };
const menuTooltipAttribs = {...tooltipMaxWidth("250px"), placement: "left", enterDelay: 1000} as TooltipProps; const menuTooltipAttribs = {...tooltipMaxWidth("250px"), placement: "left", enterDelay: 1000} as TooltipProps;
let disabledBecauseNotOwner = false;
let notOwnerTooltipText = null;
if (currentSavedView && currentSavedView.values.get("userId") != currentUserId)
{
disabledBecauseNotOwner = true;
notOwnerTooltipText = "You may not save changes to this view, because you are not its owner.";
}
const renderSavedViewsMenu = tableMetaData && ( const renderSavedViewsMenu = tableMetaData && (
<Menu <Menu
anchorEl={savedViewsMenu} anchorEl={savedViewsMenu}
@ -443,75 +463,101 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
} }
{ {
isQueryScreen && hasStorePermission && isQueryScreen && hasStorePermission &&
<Tooltip {...menuTooltipAttribs} title={<>Save your current filters, columns and settings, for quick re-use at a later time.<br /><br />You will be prompted to enter a name if you choose this option.</>}> <Tooltip {...menuTooltipAttribs} title={notOwnerTooltipText ?? <>Save your current filters, columns and settings, for quick re-use at a later time.<br /><br />You will be prompted to enter a name if you choose this option.</>}>
<MenuItem onClick={() => handleDropdownOptionClick(SAVE_OPTION)}> <span>
<ListItemIcon><Icon>save</Icon></ListItemIcon> <MenuItem disabled={disabledBecauseNotOwner} onClick={() => handleDropdownOptionClick(SAVE_OPTION)}>
{currentSavedView ? "Save..." : "Save As..."} <ListItemIcon><Icon>save</Icon></ListItemIcon>
</MenuItem> {currentSavedView ? "Save..." : "Save As..."}
</MenuItem>
</span>
</Tooltip> </Tooltip>
} }
{ {
isQueryScreen && hasStorePermission && currentSavedView != null && isQueryScreen && hasStorePermission && currentSavedView != null &&
<Tooltip {...menuTooltipAttribs} title="Change the name for this saved view."> <Tooltip {...menuTooltipAttribs} title={notOwnerTooltipText ?? "Change the name for this saved view."}>
<MenuItem disabled={currentSavedView === null} onClick={() => handleDropdownOptionClick(RENAME_OPTION)}> <span>
<ListItemIcon><Icon>edit</Icon></ListItemIcon> <MenuItem disabled={currentSavedView === null || disabledBecauseNotOwner} onClick={() => handleDropdownOptionClick(RENAME_OPTION)}>
Rename... <ListItemIcon><Icon>edit</Icon></ListItemIcon>
</MenuItem> Rename...
</MenuItem>
</span>
</Tooltip> </Tooltip>
} }
{ {
isQueryScreen && hasStorePermission && currentSavedView != null && isQueryScreen && hasStorePermission && currentSavedView != null &&
<Tooltip {...menuTooltipAttribs} title="Save a new copy this view, with a different name, separate from the original."> <Tooltip {...menuTooltipAttribs} title="Save a new copy this view, with a different name, separate from the original.">
<MenuItem disabled={currentSavedView === null} onClick={() => handleDropdownOptionClick(DUPLICATE_OPTION)}> <span>
<ListItemIcon><Icon>content_copy</Icon></ListItemIcon> <MenuItem disabled={currentSavedView === null} onClick={() => handleDropdownOptionClick(DUPLICATE_OPTION)}>
Save As... <ListItemIcon><Icon>content_copy</Icon></ListItemIcon>
</MenuItem> Save As...
</MenuItem>
</span>
</Tooltip> </Tooltip>
} }
{ {
isQueryScreen && hasDeletePermission && currentSavedView != null && isQueryScreen && hasDeletePermission && currentSavedView != null &&
<Tooltip {...menuTooltipAttribs} title="Delete this saved view."> <Tooltip {...menuTooltipAttribs} title={notOwnerTooltipText ?? "Delete this saved view."}>
<MenuItem disabled={currentSavedView === null} onClick={() => handleDropdownOptionClick(DELETE_OPTION)}> <span>
<ListItemIcon><Icon>delete</Icon></ListItemIcon> <MenuItem disabled={currentSavedView === null || disabledBecauseNotOwner} onClick={() => handleDropdownOptionClick(DELETE_OPTION)}>
Delete... <ListItemIcon><Icon>delete</Icon></ListItemIcon>
</MenuItem> Delete...
</MenuItem>
</span>
</Tooltip> </Tooltip>
} }
{ {
isQueryScreen && isQueryScreen &&
<Tooltip {...menuTooltipAttribs} title="Create a new view of this table, resetting the filters and columns to their defaults."> <Tooltip {...menuTooltipAttribs} title="Create a new view of this table, resetting the filters and columns to their defaults.">
<MenuItem onClick={() => handleDropdownOptionClick(CLEAR_OPTION)}> <span>
<ListItemIcon><Icon>monitor</Icon></ListItemIcon> <MenuItem onClick={() => handleDropdownOptionClick(CLEAR_OPTION)}>
New View <ListItemIcon><Icon>monitor</Icon></ListItemIcon>
</MenuItem> New View
</MenuItem>
</span>
</Tooltip> </Tooltip>
} }
{ {
isQueryScreen && hasSavedReportsPermission && isQueryScreen && hasSavedReportsPermission &&
<Tooltip {...menuTooltipAttribs} title="Create a new Saved Report using your current view of this table as a starting point."> <Tooltip {...menuTooltipAttribs} title="Create a new Saved Report using your current view of this table as a starting point.">
<MenuItem onClick={() => handleDropdownOptionClick(NEW_REPORT_OPTION)}> <span>
<ListItemIcon><Icon>article</Icon></ListItemIcon> <MenuItem onClick={() => handleDropdownOptionClick(NEW_REPORT_OPTION)}>
Create Report from Current View <ListItemIcon><Icon>article</Icon></ListItemIcon>
</MenuItem> Create Report from Current View
</MenuItem>
</span>
</Tooltip> </Tooltip>
} }
{ {
isQueryScreen && <Divider/> isQueryScreen && <Divider />
} }
<MenuItem disabled style={{"opacity": "initial"}}><b>Your Saved Views</b></MenuItem> <MenuItem disabled style={{"opacity": "initial"}}><b>Your Saved Views</b></MenuItem>
{ {
savedViews && savedViews.length > 0 ? ( yourSavedViews && yourSavedViews.length > 0 ? (
savedViews.map((record: QRecord, index: number) => yourSavedViews.map((record: QRecord, index: number) =>
<MenuItem sx={{paddingLeft: "50px"}} key={`savedFiler-${index}`} onClick={() => handleSavedViewRecordOnClick(record)}> <MenuItem sx={{paddingLeft: "50px"}} key={`savedFiler-${index}`} onClick={() => handleSavedViewRecordOnClick(record)}>
{record.values.get("label")} {record.values.get("label")}
</MenuItem> </MenuItem>
) )
): ( ) : (
<MenuItem disabled sx={{opacity: "1 !important"}}> <MenuItem disabled sx={{opacity: "1 !important"}}>
<i>You do not have any saved views for this table.</i> <i>You do not have any saved views for this table.</i>
</MenuItem> </MenuItem>
) )
} }
<MenuItem disabled style={{"opacity": "initial"}}><b>Views Shared with you</b></MenuItem>
{
viewsSharedWithYou && viewsSharedWithYou.length > 0 ? (
viewsSharedWithYou.map((record: QRecord, index: number) =>
<MenuItem sx={{paddingLeft: "50px"}} key={`savedFiler-${index}`} onClick={() => handleSavedViewRecordOnClick(record)}>
{record.values.get("label")}
</MenuItem>
)
) : (
<MenuItem disabled sx={{opacity: "1 !important"}}>
<i>You do not have any views shared with you for this table.</i>
</MenuItem>
)
}
</Menu> </Menu>
); );
@ -520,7 +566,7 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
let buttonBorder = colors.grayLines.main; let buttonBorder = colors.grayLines.main;
let buttonColor = colors.gray.main; let buttonColor = colors.gray.main;
if(currentSavedView) if (currentSavedView)
{ {
if (viewIsModified) if (viewIsModified)
{ {
@ -548,23 +594,23 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
color: buttonColor, color: buttonColor,
backgroundColor: buttonBackground, backgroundColor: buttonBackground,
} }
} };
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
function isSaveButtonDisabled(): boolean function isSaveButtonDisabled(): boolean
{ {
if(isSubmitting) if (isSubmitting)
{ {
return (true); return (true);
} }
const haveInputText = (savedViewNameInputValue != null && savedViewNameInputValue.trim() != "") const haveInputText = (savedViewNameInputValue != null && savedViewNameInputValue.trim() != "");
if(isSaveFilterAs || isRenameFilter || currentSavedView == null) if (isSaveFilterAs || isRenameFilter || currentSavedView == null)
{ {
if(!haveInputText) if (!haveInputText)
{ {
return (true); return (true);
} }
@ -593,7 +639,7 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
fontWeight: 500, fontWeight: 500,
fontSize: "0.875rem", fontSize: "0.875rem",
p: "0.5rem", p: "0.5rem",
... buttonStyles ...buttonStyles
}} }}
> >
<Icon sx={{mr: "0.5rem"}}>save</Icon> <Icon sx={{mr: "0.5rem"}}>save</Icon>
@ -624,7 +670,7 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
</> </>
} }
<Button disableRipple={true} sx={{color: colors.gray.main, ... linkButtonStyle}} onClick={() => handleDropdownOptionClick(CLEAR_OPTION)}>Reset All Changes</Button> <Button disableRipple={true} sx={{color: colors.gray.main, ...linkButtonStyle}} onClick={() => handleDropdownOptionClick(CLEAR_OPTION)}>Reset All Changes</Button>
</> </>
} }
{ {
@ -635,16 +681,20 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
{ {
viewDiffs.map((s: string, i: number) => <li key={i}>{s}</li>) viewDiffs.map((s: string, i: number) => <li key={i}>{s}</li>)
} }
</ul></>}> </ul>
{
notOwnerTooltipText && <i>{notOwnerTooltipText}</i>
}
</>}>
<Box display="inline" sx={{...linkButtonStyle, p: 0, cursor: "default", position: "relative", top: "-1px"}}>{viewDiffs.length} Unsaved Change{viewDiffs.length == 1 ? "" : "s"}</Box> <Box display="inline" sx={{...linkButtonStyle, p: 0, cursor: "default", position: "relative", top: "-1px"}}>{viewDiffs.length} Unsaved Change{viewDiffs.length == 1 ? "" : "s"}</Box>
</Tooltip> </Tooltip>
<Button disableRipple={true} sx={linkButtonStyle} onClick={() => handleDropdownOptionClick(SAVE_OPTION)}>Save&hellip;</Button> {disabledBecauseNotOwner ? <>&nbsp;&nbsp;</> : <Button disableRipple={true} sx={linkButtonStyle} onClick={() => handleDropdownOptionClick(SAVE_OPTION)}>Save&hellip;</Button>}
{/* vertical rule */} {/* vertical rule */}
<Box display="inline-block" borderLeft={`1px solid ${colors.grayLines.main}`} height="1rem" width="1px" position="relative" /> <Box display="inline-block" borderLeft={`1px solid ${colors.grayLines.main}`} height="1rem" width="1px" position="relative" />
<Button disableRipple={true} sx={{color: colors.gray.main, ... linkButtonStyle}} onClick={() => handleSavedViewRecordOnClick(currentSavedView)}>Reset All Changes</Button> <Button disableRipple={true} sx={{color: colors.gray.main, ...linkButtonStyle}} onClick={() => handleSavedViewRecordOnClick(currentSavedView)}>Reset All Changes</Button>
</> </>
} }
{ {
@ -663,16 +713,17 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
{ {
viewDiffs.map((s: string, i: number) => <li key={i}>{s}</li>) viewDiffs.map((s: string, i: number) => <li key={i}>{s}</li>)
} }
</ul></>}> </ul>
</>}>
<Box display="inline" ml="0.25rem" mr="0.25rem" sx={{...linkButtonStyle, p: 0, cursor: "default", position: "relative", top: "-1px"}}>with {viewDiffs.length} Change{viewDiffs.length == 1 ? "" : "s"}</Box> <Box display="inline" ml="0.25rem" mr="0.25rem" sx={{...linkButtonStyle, p: 0, cursor: "default", position: "relative", top: "-1px"}}>with {viewDiffs.length} Change{viewDiffs.length == 1 ? "" : "s"}</Box>
</Tooltip> </Tooltip>
<Button disableRipple={true} sx={{color: colors.gray.main, ... linkButtonStyle}} onClick={() => handleSavedViewRecordOnClick(currentSavedView)}>Reset Changes</Button> <Button disableRipple={true} sx={{color: colors.gray.main, ...linkButtonStyle}} onClick={() => handleSavedViewRecordOnClick(currentSavedView)}>Reset Changes</Button>
</> </>
} }
{/* vertical rule */} {/* vertical rule */}
<Box display="inline-block" ml="0.25rem" borderLeft={`1px solid ${colors.grayLines.main}`} height="1rem" width="1px" position="relative" /> <Box display="inline-block" ml="0.25rem" borderLeft={`1px solid ${colors.grayLines.main}`} height="1rem" width="1px" position="relative" />
<Button disableRipple={true} sx={{color: colors.gray.main, ... linkButtonStyle}} onClick={() => handleDropdownOptionClick(CLEAR_OPTION)}>Reset to New View</Button> <Button disableRipple={true} sx={{color: colors.gray.main, ...linkButtonStyle}} onClick={() => handleDropdownOptionClick(CLEAR_OPTION)}>Reset to New View</Button>
</Box> </Box>
} }
</Box> </Box>
@ -702,15 +753,15 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
) : ( ) : (
isSaveFilterAs ? ( isSaveFilterAs ? (
<DialogTitle id="alert-dialog-title">Save View As</DialogTitle> <DialogTitle id="alert-dialog-title">Save View As</DialogTitle>
):( ) : (
isRenameFilter ? ( isRenameFilter ? (
<DialogTitle id="alert-dialog-title">Rename View</DialogTitle> <DialogTitle id="alert-dialog-title">Rename View</DialogTitle>
):( ) : (
<DialogTitle id="alert-dialog-title">Update Existing View</DialogTitle> <DialogTitle id="alert-dialog-title">Update Existing View</DialogTitle>
) )
) )
) )
):( ) : (
<DialogTitle id="alert-dialog-title">Save New View</DialogTitle> <DialogTitle id="alert-dialog-title">Save New View</DialogTitle>
) )
} }
@ -721,12 +772,12 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
</Box> </Box>
) : ("")} ) : ("")}
{ {
(! currentSavedView || isSaveFilterAs || isRenameFilter) && ! isDeleteFilter ? ( (!currentSavedView || isSaveFilterAs || isRenameFilter) && !isDeleteFilter ? (
<Box> <Box>
{ {
isSaveFilterAs ? ( isSaveFilterAs ? (
<Box mb={3}>Enter a name for this new saved view.</Box> <Box mb={3}>Enter a name for this new saved view.</Box>
):( ) : (
<Box mb={3}>Enter a new name for this saved view.</Box> <Box mb={3}>Enter a new name for this saved view.</Box>
) )
} }
@ -744,10 +795,10 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
}} }}
/> />
</Box> </Box>
):( ) : (
isDeleteFilter ? ( isDeleteFilter ? (
<Box>Are you sure you want to delete the view {`'${currentSavedView?.values.get("label")}'`}?</Box> <Box>Are you sure you want to delete the view {`'${currentSavedView?.values.get("label")}'`}?</Box>
):( ) : (
<Box>Are you sure you want to update the view {`'${currentSavedView?.values.get("label")}'`}?</Box> <Box>Are you sure you want to update the view {`'${currentSavedView?.values.get("label")}'`}?</Box>
) )
) )
@ -759,7 +810,7 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
isDeleteFilter ? isDeleteFilter ?
<QDeleteButton onClickHandler={handleFilterDialogButtonOnClick} disabled={isSubmitting} /> <QDeleteButton onClickHandler={handleFilterDialogButtonOnClick} disabled={isSubmitting} />
: :
<QSaveButton label="Save" onClickHandler={handleFilterDialogButtonOnClick} disabled={isSaveButtonDisabled()}/> <QSaveButton label="Save" onClickHandler={handleFilterDialogButtonOnClick} disabled={isSaveButtonDisabled()} />
} }
</DialogActions> </DialogActions>
</Dialog> </Dialog>

View File

@ -57,7 +57,7 @@ export default function AssignFilterVariable({valueIndex, field, valueChangeHand
return <Box display="flex" alignItems="flex-end"> return <Box display="flex" alignItems="flex-end">
<Box> <Box>
<Tooltip title={`Use a variable as the value for the ${field.name} field`} placement="bottom"> <Tooltip title={`Use a variable as the value for the ${field.label} field`} placement="bottom">
<Icon fontSize="small" color="info" sx={{mx: 0.25, cursor: "pointer", position: "relative", top: "2px"}} onClick={handleVariableButtonOnClick}>functions</Icon> <Icon fontSize="small" color="info" sx={{mx: 0.25, cursor: "pointer", position: "relative", top: "2px"}} onClick={handleVariableButtonOnClick}>functions</Icon>
</Tooltip> </Tooltip>
</Box> </Box>

View File

@ -196,84 +196,120 @@ export default function CriteriaDateField({valueIndex, label, idPrefix, field, c
setTimeout(() => setForceAdvancedDateTimeDialogOpen(false), 100); setTimeout(() => setForceAdvancedDateTimeDialogOpen(false), 100);
} }
const makeFilterVariableTextField = (expression: FilterVariableExpression, valueIndex: number = 0, label = "Value", idPrefix = "value-") =>
{
const clearValue = (event: React.MouseEvent<HTMLAnchorElement> | React.MouseEvent<HTMLButtonElement>, index: number) =>
{
valueChangeHandler(event, index, "");
document.getElementById(`${idPrefix}${criteria.id}`).focus();
};
const inputProps2: any = {};
inputProps2.endAdornment = (
<InputAdornment position="end">
<IconButton sx={{visibility: expression ? "visible" : "hidden"}} onClick={(event) => clearValue(event, valueIndex)}>
<Icon>closer</Icon>
</IconButton>
</InputAdornment>
);
return <NoWrapTooltip title={<EvaluatedExpression field={field} expression={expression} />} placement="bottom" enterDelay={1000} sx={{marginLeft: "-75px !important", marginTop: "-8px !important"}}><TextField
id={`${idPrefix}${criteria.id}`}
label={label}
variant="standard"
autoComplete="off"
InputProps={{disabled: true, readOnly: true, unselectable: "off", ...inputProps2}}
InputLabelProps={{shrink: true}}
value="${VARIABLE}"
fullWidth
/></NoWrapTooltip>;
};
return <Box display="flex" alignItems="flex-end"> return <Box display="flex" alignItems="flex-end">
{ {
isExpression ? makeDateTimeExpressionTextField(criteria.values[valueIndex], valueIndex, label, idPrefix) isExpression ?
currentExpression?.type == "FilterVariableExpression" ? (
makeFilterVariableTextField(criteria.values[valueIndex], valueIndex, label, idPrefix)
) : (
makeDateTimeExpressionTextField(criteria.values[valueIndex], valueIndex, label, idPrefix)
)
: makeTextField(field, criteria, valueChangeHandler, valueIndex, label, idPrefix, allowVariables) : makeTextField(field, criteria, valueChangeHandler, valueIndex, label, idPrefix, allowVariables)
} }
<Box> {
<Tooltip title={`Choose a common relative ${field.type == QFieldType.DATE ? "date" : "date-time"} expression`} placement="bottom"> (!isExpression || currentExpression?.type != "FilterVariableExpression") && (
<Icon fontSize="small" color="info" sx={{mx: 0.25, cursor: "pointer", position: "relative", top: "2px"}} onClick={openRelativeDateTimeMenu}>date_range</Icon> <><Box>
</Tooltip> <Tooltip title={`Choose a common relative ${field.type == QFieldType.DATE ? "date" : "date-time"} expression`} placement="bottom">
<Menu <Icon fontSize="small" color="info" sx={{mx: 0.25, cursor: "pointer", position: "relative", top: "2px"}} onClick={openRelativeDateTimeMenu}>date_range</Icon>
open={relativeDateTimeOpen} </Tooltip>
anchorEl={relativeDateTimeMenuAnchorElement} <Menu
transformOrigin={{horizontal: "left", vertical: "top"}} open={relativeDateTimeOpen}
onClose={closeRelativeDateTimeMenu} anchorEl={relativeDateTimeMenuAnchorElement}
> transformOrigin={{horizontal: "left", vertical: "top"}}
{ onClose={closeRelativeDateTimeMenu}
field.type == QFieldType.DATE ? >
<Box display="flex"> {field.type == QFieldType.DATE ?
<Box> <Box display="flex">
{tooltipMenuItemFromExpression(valueIndex, "left", newNowWithOffsetExpression("MINUS", 7, "DAYS"))} <Box>
{tooltipMenuItemFromExpression(valueIndex, "left", newNowWithOffsetExpression("MINUS", 14, "DAYS"))} {tooltipMenuItemFromExpression(valueIndex, "left", newNowWithOffsetExpression("MINUS", 7, "DAYS"))}
{tooltipMenuItemFromExpression(valueIndex, "left", newNowWithOffsetExpression("MINUS", 30, "DAYS"))} {tooltipMenuItemFromExpression(valueIndex, "left", newNowWithOffsetExpression("MINUS", 14, "DAYS"))}
{tooltipMenuItemFromExpression(valueIndex, "left", newNowWithOffsetExpression("MINUS", 90, "DAYS"))} {tooltipMenuItemFromExpression(valueIndex, "left", newNowWithOffsetExpression("MINUS", 30, "DAYS"))}
{tooltipMenuItemFromExpression(valueIndex, "left", newNowWithOffsetExpression("MINUS", 180, "DAYS"))} {tooltipMenuItemFromExpression(valueIndex, "left", newNowWithOffsetExpression("MINUS", 90, "DAYS"))}
{tooltipMenuItemFromExpression(valueIndex, "left", newNowWithOffsetExpression("MINUS", 1, "YEARS"))} {tooltipMenuItemFromExpression(valueIndex, "left", newNowWithOffsetExpression("MINUS", 180, "DAYS"))}
<Divider /> {tooltipMenuItemFromExpression(valueIndex, "left", newNowWithOffsetExpression("MINUS", 1, "YEARS"))}
<Tooltip title="Define a custom expression" placement="left"> <Divider />
<MenuItem onClick={doForceAdvancedDateTimeDialogOpen}>Custom</MenuItem> <Tooltip title="Define a custom expression" placement="left">
</Tooltip> <MenuItem onClick={doForceAdvancedDateTimeDialogOpen}>Custom</MenuItem>
</Tooltip>
</Box>
<Box>
{tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("THIS", "DAYS"))}
{tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("LAST", "DAYS"))}
{tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("THIS", "WEEKS"))}
{tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("LAST", "WEEKS"))}
{tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("THIS", "MONTHS"))}
{tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("LAST", "MONTHS"))}
{tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("THIS", "YEARS"))}
{tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("LAST", "YEARS"))}
</Box>
</Box> </Box>
<Box> :
{tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("THIS", "DAYS"))} <Box display="flex">
{tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("LAST", "DAYS"))} <Box>
{tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("THIS", "WEEKS"))} {tooltipMenuItemFromExpression(valueIndex, "left", newNowWithOffsetExpression("MINUS", 1, "HOURS"))}
{tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("LAST", "WEEKS"))} {tooltipMenuItemFromExpression(valueIndex, "left", newNowWithOffsetExpression("MINUS", 12, "HOURS"))}
{tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("THIS", "MONTHS"))} {tooltipMenuItemFromExpression(valueIndex, "left", newNowWithOffsetExpression("MINUS", 24, "HOURS"))}
{tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("LAST", "MONTHS"))} {tooltipMenuItemFromExpression(valueIndex, "left", newNowWithOffsetExpression("MINUS", 7, "DAYS"))}
{tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("THIS", "YEARS"))} {tooltipMenuItemFromExpression(valueIndex, "left", newNowWithOffsetExpression("MINUS", 14, "DAYS"))}
{tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("LAST", "YEARS"))} {tooltipMenuItemFromExpression(valueIndex, "left", newNowWithOffsetExpression("MINUS", 30, "DAYS"))}
</Box> {tooltipMenuItemFromExpression(valueIndex, "left", newNowWithOffsetExpression("MINUS", 90, "DAYS"))}
</Box> {tooltipMenuItemFromExpression(valueIndex, "left", newNowWithOffsetExpression("MINUS", 180, "DAYS"))}
: {tooltipMenuItemFromExpression(valueIndex, "left", newNowWithOffsetExpression("MINUS", 1, "YEARS"))}
<Box display="flex"> <Divider />
<Box> <Tooltip title="Define a custom expression" placement="left">
{tooltipMenuItemFromExpression(valueIndex, "left", newNowWithOffsetExpression("MINUS", 1, "HOURS"))} <MenuItem onClick={doForceAdvancedDateTimeDialogOpen}>Custom</MenuItem>
{tooltipMenuItemFromExpression(valueIndex, "left", newNowWithOffsetExpression("MINUS", 12, "HOURS"))} </Tooltip>
{tooltipMenuItemFromExpression(valueIndex, "left", newNowWithOffsetExpression("MINUS", 24, "HOURS"))} </Box>
{tooltipMenuItemFromExpression(valueIndex, "left", newNowWithOffsetExpression("MINUS", 7, "DAYS"))} <Box>
{tooltipMenuItemFromExpression(valueIndex, "left", newNowWithOffsetExpression("MINUS", 14, "DAYS"))} {tooltipMenuItemFromExpression(valueIndex, "right", newNowExpression())}
{tooltipMenuItemFromExpression(valueIndex, "left", newNowWithOffsetExpression("MINUS", 30, "DAYS"))} {tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("THIS", "HOURS"))}
{tooltipMenuItemFromExpression(valueIndex, "left", newNowWithOffsetExpression("MINUS", 90, "DAYS"))} {tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("LAST", "HOURS"))}
{tooltipMenuItemFromExpression(valueIndex, "left", newNowWithOffsetExpression("MINUS", 180, "DAYS"))} {tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("THIS", "DAYS"))}
{tooltipMenuItemFromExpression(valueIndex, "left", newNowWithOffsetExpression("MINUS", 1, "YEARS"))} {tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("LAST", "DAYS"))}
<Divider /> {tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("THIS", "WEEKS"))}
<Tooltip title="Define a custom expression" placement="left"> {tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("LAST", "WEEKS"))}
<MenuItem onClick={doForceAdvancedDateTimeDialogOpen}>Custom</MenuItem> {tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("THIS", "MONTHS"))}
</Tooltip> {tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("LAST", "MONTHS"))}
</Box> {tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("THIS", "YEARS"))}
<Box> {tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("LAST", "YEARS"))}
{tooltipMenuItemFromExpression(valueIndex, "right", newNowExpression())} </Box>
{tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("THIS", "HOURS"))} </Box>}
{tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("LAST", "HOURS"))} </Menu>
{tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("THIS", "DAYS"))} </Box><Box>
{tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("LAST", "DAYS"))} <AdvancedDateTimeFilterValues type={field.type} expression={currentExpression} onSave={(expression: any) => saveNewDateTimeExpression(valueIndex, expression)} forcedOpen={forceAdvancedDateTimeDialogOpen} />
{tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("THIS", "WEEKS"))} </Box></>
{tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("LAST", "WEEKS"))} )
{tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("THIS", "MONTHS"))} }
{tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("LAST", "MONTHS"))}
{tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("THIS", "YEARS"))}
{tooltipMenuItemFromExpression(valueIndex, "right", newThisOrLastPeriodExpression("LAST", "YEARS"))}
</Box>
</Box>
}
</Menu>
</Box>
<Box>
<AdvancedDateTimeFilterValues type={field.type} expression={currentExpression} onSave={(expression: any) => saveNewDateTimeExpression(valueIndex, expression)} forcedOpen={forceAdvancedDateTimeDialogOpen} />
</Box>
</Box>; </Box>;
} }

View File

@ -28,8 +28,8 @@ import Box from "@mui/material/Box";
import Button from "@mui/material/Button/Button"; import Button from "@mui/material/Button/Button";
import Icon from "@mui/material/Icon/Icon"; import Icon from "@mui/material/Icon/Icon";
import {GridFilterPanelProps, GridSlotsComponentsProps} from "@mui/x-data-grid-pro"; import {GridFilterPanelProps, GridSlotsComponentsProps} from "@mui/x-data-grid-pro";
import React, {forwardRef, useReducer} from "react";
import {FilterCriteriaRow, getDefaultCriteriaValue} from "qqq/components/query/FilterCriteriaRow"; import {FilterCriteriaRow, getDefaultCriteriaValue} from "qqq/components/query/FilterCriteriaRow";
import React, {forwardRef, useReducer} from "react";
declare module "@mui/x-data-grid" declare module "@mui/x-data-grid"
@ -49,7 +49,7 @@ declare module "@mui/x-data-grid"
export class QFilterCriteriaWithId extends QFilterCriteria export class QFilterCriteriaWithId extends QFilterCriteria
{ {
id: number id: number;
} }
@ -62,6 +62,7 @@ export const CustomFilterPanel = forwardRef<any, GridFilterPanelProps>(
const [, forceUpdate] = useReducer((x) => x + 1, 0); const [, forceUpdate] = useReducer((x) => x + 1, 0);
const queryFilter = props.queryFilter; const queryFilter = props.queryFilter;
// console.log(`CustomFilterPanel: filter: ${JSON.stringify(queryFilter)}`); // console.log(`CustomFilterPanel: filter: ${JSON.stringify(queryFilter)}`);
function focusLastField() function focusLastField()
@ -124,7 +125,7 @@ export const CustomFilterPanel = forwardRef<any, GridFilterPanelProps>(
} }
} }
if(queryFilter.criteria.length == 1 && !queryFilter.criteria[0].fieldName) if (queryFilter.criteria.length == 1 && !queryFilter.criteria[0].fieldName)
{ {
focusLastField(); focusLastField();
} }
@ -142,7 +143,7 @@ export const CustomFilterPanel = forwardRef<any, GridFilterPanelProps>(
{ {
queryFilter.criteria[index] = newCriteria; queryFilter.criteria[index] = newCriteria;
clearTimeout(debounceTimeout) clearTimeout(debounceTimeout);
debounceTimeout = setTimeout(() => props.updateFilter(queryFilter), needDebounce ? 500 : 1); debounceTimeout = setTimeout(() => props.updateFilter(queryFilter), needDebounce ? 500 : 1);
forceUpdate(); forceUpdate();
@ -178,6 +179,7 @@ export const CustomFilterPanel = forwardRef<any, GridFilterPanelProps>(
updateCriteria={(newCriteria, needDebounce) => updateCriteria(newCriteria, index, needDebounce)} updateCriteria={(newCriteria, needDebounce) => updateCriteria(newCriteria, index, needDebounce)}
removeCriteria={() => removeCriteria(index)} removeCriteria={() => removeCriteria(index)}
updateBooleanOperator={(newValue) => updateBooleanOperator(newValue)} updateBooleanOperator={(newValue) => updateBooleanOperator(newValue)}
queryScreenUsage={props.queryScreenUsage}
/> />
{/*JSON.stringify(criteria)*/} {/*JSON.stringify(criteria)*/}
</Box> </Box>

View File

@ -35,6 +35,7 @@ import TextField from "@mui/material/TextField";
import Tooltip from "@mui/material/Tooltip"; import Tooltip from "@mui/material/Tooltip";
import FieldAutoComplete from "qqq/components/misc/FieldAutoComplete"; import FieldAutoComplete from "qqq/components/misc/FieldAutoComplete";
import FilterCriteriaRowValues from "qqq/components/query/FilterCriteriaRowValues"; import FilterCriteriaRowValues from "qqq/components/query/FilterCriteriaRowValues";
import {QueryScreenUsage} from "qqq/pages/records/query/RecordQuery";
import FilterUtils from "qqq/utils/qqq/FilterUtils"; import FilterUtils from "qqq/utils/qqq/FilterUtils";
import React, {ReactNode, SyntheticEvent, useState} from "react"; import React, {ReactNode, SyntheticEvent, useState} from "react";
@ -197,6 +198,7 @@ interface FilterCriteriaRowProps
updateCriteria: (newCriteria: QFilterCriteria, needDebounce: boolean) => void; updateCriteria: (newCriteria: QFilterCriteria, needDebounce: boolean) => void;
removeCriteria: () => void; removeCriteria: () => void;
updateBooleanOperator: (newValue: string) => void; updateBooleanOperator: (newValue: string) => void;
queryScreenUsage?: QueryScreenUsage;
} }
FilterCriteriaRow.defaultProps = FilterCriteriaRow.defaultProps =
@ -265,7 +267,7 @@ export function validateCriteria(criteria: QFilterCriteria, operatorSelectedValu
return {criteriaIsValid, criteriaStatusTooltip}; return {criteriaIsValid, criteriaStatusTooltip};
} }
export function FilterCriteriaRow({id, index, tableMetaData, metaData, criteria, booleanOperator, updateCriteria, removeCriteria, updateBooleanOperator}: FilterCriteriaRowProps): JSX.Element export function FilterCriteriaRow({id, index, tableMetaData, metaData, criteria, booleanOperator, updateCriteria, removeCriteria, updateBooleanOperator, queryScreenUsage}: FilterCriteriaRowProps): JSX.Element
{ {
// console.log(`FilterCriteriaRow: criteria: ${JSON.stringify(criteria)}`); // console.log(`FilterCriteriaRow: criteria: ${JSON.stringify(criteria)}`);
const [operatorSelectedValue, setOperatorSelectedValue] = useState(null as OperatorOption); const [operatorSelectedValue, setOperatorSelectedValue] = useState(null as OperatorOption);
@ -513,6 +515,7 @@ export function FilterCriteriaRow({id, index, tableMetaData, metaData, criteria,
field={field} field={field}
table={fieldTable} table={fieldTable}
valueChangeHandler={(event, valueIndex, newValue) => handleValueChange(event, valueIndex, newValue)} valueChangeHandler={(event, valueIndex, newValue) => handleValueChange(event, valueIndex, newValue)}
queryScreenUsage={queryScreenUsage}
/> />
</Box> </Box>
<Box display="inline-block"> <Box display="inline-block">

View File

@ -220,6 +220,8 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC
forceUpdate(); forceUpdate();
} }
const isExpression = criteria.values && criteria.values[0] && criteria.values[0].type;
switch (operatorOption.valueMode) switch (operatorOption.valueMode)
{ {
case ValueMode.NONE: case ValueMode.NONE:
@ -227,18 +229,18 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC
case ValueMode.SINGLE: case ValueMode.SINGLE:
return makeTextField(field, criteria, valueChangeHandler, 0, undefined, undefined, allowVariables); return makeTextField(field, criteria, valueChangeHandler, 0, undefined, undefined, allowVariables);
case ValueMode.SINGLE_DATE: case ValueMode.SINGLE_DATE:
return <CriteriaDateField field={field} valueChangeHandler={valueChangeHandler} criteria={criteria} />; return <CriteriaDateField field={field} valueChangeHandler={valueChangeHandler} criteria={criteria} allowVariables={allowVariables} />;
case ValueMode.DOUBLE_DATE: case ValueMode.DOUBLE_DATE:
return <Box> return <Box>
<CriteriaDateField field={field} valueChangeHandler={valueChangeHandler} criteria={criteria} valueIndex={0} label="From" idPrefix="from-" /> <CriteriaDateField field={field} valueChangeHandler={valueChangeHandler} criteria={criteria} valueIndex={0} label="From" idPrefix="from-" allowVariables={allowVariables} />
<CriteriaDateField field={field} valueChangeHandler={valueChangeHandler} criteria={criteria} valueIndex={1} label="To" idPrefix="to-" /> <CriteriaDateField field={field} valueChangeHandler={valueChangeHandler} criteria={criteria} valueIndex={1} label="To" idPrefix="to-" allowVariables={allowVariables} />
</Box>; </Box>;
case ValueMode.SINGLE_DATE_TIME: case ValueMode.SINGLE_DATE_TIME:
return <CriteriaDateField field={field} valueChangeHandler={valueChangeHandler} criteria={criteria} allowVariables={allowVariables} />; return <CriteriaDateField field={field} valueChangeHandler={valueChangeHandler} criteria={criteria} allowVariables={allowVariables} />;
case ValueMode.DOUBLE_DATE_TIME: case ValueMode.DOUBLE_DATE_TIME:
return <Box> return <Box>
<CriteriaDateField field={field} valueChangeHandler={valueChangeHandler} criteria={criteria} valueIndex={0} label="From" idPrefix="from-" /> <CriteriaDateField field={field} valueChangeHandler={valueChangeHandler} criteria={criteria} valueIndex={0} label="From" idPrefix="from-" allowVariables={allowVariables} />
<CriteriaDateField field={field} valueChangeHandler={valueChangeHandler} criteria={criteria} valueIndex={1} label="To" idPrefix="to-" /> <CriteriaDateField field={field} valueChangeHandler={valueChangeHandler} criteria={criteria} valueIndex={1} label="To" idPrefix="to-" allowVariables={allowVariables} />
</Box>; </Box>;
case ValueMode.DOUBLE: case ValueMode.DOUBLE:
return <Box> return <Box>
@ -279,19 +281,30 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC
{ {
selectedPossibleValue = criteria.values[0]; selectedPossibleValue = criteria.values[0];
} }
return <Box mb={-1.5}> return <Box display="flex">
<DynamicSelect {
tableName={table.name} isExpression ? (
fieldName={field.name} makeTextField(field, criteria, valueChangeHandler, 0, undefined, undefined, allowVariables)
overrideId={field.name + "-single-" + criteria.id} ) : (
key={field.name + "-single-" + criteria.id} <Box width={"100%"}>
fieldLabel="Value" <DynamicSelect
initialValue={selectedPossibleValue?.id} tableName={table.name}
initialDisplayValue={selectedPossibleValue?.label} fieldName={field.name}
inForm={false} overrideId={field.name + "-single-" + criteria.id}
onChange={(value: any) => valueChangeHandler(null, 0, value)} key={field.name + "-single-" + criteria.id}
variant="standard" fieldLabel="Value"
/> initialValue={selectedPossibleValue?.id}
initialDisplayValue={selectedPossibleValue?.label}
inForm={false}
onChange={(value: any) => valueChangeHandler(null, 0, value)}
variant="standard"
/>
</Box>
)
}
{
allowVariables && !isExpression && <Box mt={2.0}><AssignFilterVariable field={field} valueChangeHandler={valueChangeHandler} valueIndex={0} /></Box>
}
</Box>; </Box>;
case ValueMode.PVS_MULTI: case ValueMode.PVS_MULTI:
console.log("Doing pvs multi: " + criteria.values); console.log("Doing pvs multi: " + criteria.values);

View File

@ -507,7 +507,7 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
////////////////////////////// //////////////////////////////
// return the button & menu // // return the button & menu //
////////////////////////////// //////////////////////////////
const widthAndMaxWidth = (fieldMetaData?.type == QFieldType.DATE_TIME) ? 295 : 250; const widthAndMaxWidth = (fieldMetaData?.type == QFieldType.DATE_TIME) ? 315 : 250;
return ( return (
<> <>
{button} {button}

View File

@ -24,9 +24,8 @@ import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QT
import {QJobComplete} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobComplete"; import {QJobComplete} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobComplete";
import {QJobError} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobError"; import {QJobError} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobError";
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord"; import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
import {Alert} from "@mui/material"; import {Alert, Box} from "@mui/material";
import Autocomplete from "@mui/material/Autocomplete"; import Autocomplete from "@mui/material/Autocomplete";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import Card from "@mui/material/Card"; import Card from "@mui/material/Card";
import Icon from "@mui/material/Icon"; import Icon from "@mui/material/Icon";
@ -370,15 +369,15 @@ export default function ShareModal({open, onClose, tableMetaData, record}: Share
<Card sx={{my: 5, mx: "auto", p: 3}}> <Card sx={{my: 5, mx: "auto", p: 3}}>
{/* header */} {/* header */}
<Box display="flex" flexDirection="row" justifyContent="space-between" alignItems="flex-start"> <Box display="flex" flexDirection="row" justifyContent="space-between" alignItems="flex-start" maxWidth="590px">
<Typography variant="h4" pb={1} fontWeight="600"> <Typography variant="h4" pb={1} fontWeight="600">
Share {tableMetaData.label}: {record?.recordLabel ?? record?.values?.get(tableMetaData.primaryKeyField) ?? "Unknown"} Share {tableMetaData.label}: {record?.recordLabel ?? record?.values?.get(tableMetaData.primaryKeyField) ?? "Unknown"}
<Box color={colors.gray.main} pb={"0.5rem"} fontSize={"0.875rem"} fontWeight="400" maxWidth="590px"> <Box color={colors.gray.main} pb={"0.5rem"} fontSize={"0.875rem"} fontWeight="400">
{/* todo move to helpContent (what do we attach the meta-data too??) */} {/* todo move to helpContent (what do we attach the meta-data too??) */}
Select a user or a group to share this record with. Select a user or a group to share this record with.
You can choose if they should only be able to Read the record, or also make Edits to it. {/*You can choose if they should only be able to Read the record, or also make Edits to it.*/}
</Box> </Box>
<Box fontSize={14} maxWidth="590px" pb={1} fontWeight="300"> <Box fontSize={14} pb={1} fontWeight="300">
{alert && <Alert color="error" onClose={() => setAlert(null)}>{alert}</Alert>} {alert && <Alert color="error" onClose={() => setAlert(null)}>{alert}</Alert>}
{statusString} {statusString}
{!alert && !statusString && (<>&nbsp;</>)} {!alert && !statusString && (<>&nbsp;</>)}
@ -390,7 +389,7 @@ export default function ShareModal({open, onClose, tableMetaData, record}: Share
<Box pb={3} display="flex" flexDirection="column"> <Box pb={3} display="flex" flexDirection="column">
{/* row for adding a new share */} {/* row for adding a new share */}
<Box display="flex" flexDirection="row" alignItems="center"> <Box display="flex" flexDirection="row" alignItems="center">
<Box width="350px" pr={2} mb={-1.5}> <Box width="550px" pr={2} mb={-1.5}>
<DynamicSelect <DynamicSelect
possibleValueSourceName={shareableTableMetaData.audiencePossibleValueSourceName} possibleValueSourceName={shareableTableMetaData.audiencePossibleValueSourceName}
fieldLabel="User or Group" // todo should come from shareableTableMetaData fieldLabel="User or Group" // todo should come from shareableTableMetaData
@ -400,9 +399,12 @@ export default function ShareModal({open, onClose, tableMetaData, record}: Share
onChange={handleAudienceChange} onChange={handleAudienceChange}
/> />
</Box> </Box>
<Box width="180px" pr={2}> {/*
{renderScopeDropdown("new-share-scope", defaultScope, handleScopeChange)} when turning scope back on, change width of audience box to 350px
</Box> <Box width="180px" pr={2}>
{renderScopeDropdown("new-share-scope", defaultScope, handleScopeChange)}
</Box>
*/}
<Box> <Box>
<Tooltip title={selectedAudienceId == null ? "Select a user or group to share with." : null}> <Tooltip title={selectedAudienceId == null ? "Select a user or group to share with." : null}>
<span> <span>
@ -429,8 +431,11 @@ export default function ShareModal({open, onClose, tableMetaData, record}: Share
currentShares.map((share) => ( currentShares.map((share) => (
<Box key={share.shareId} display="flex" justifyContent="space-between" alignItems="center" p="0.25rem" pb="0.75rem" fontSize="1rem"> <Box key={share.shareId} display="flex" justifyContent="space-between" alignItems="center" p="0.25rem" pb="0.75rem" fontSize="1rem">
<Box display="flex" alignItems="center"> <Box display="flex" alignItems="center">
<Box width="310px" pl="1rem">{share.audienceLabel}</Box> <Box width="490px" pl="1rem">{share.audienceLabel}</Box>
<Box width="160px">{renderScopeDropdown(`scope-${share.shareId}`, getScopeOption(share.scopeId), (event: React.SyntheticEvent, value: any | any[], reason: string) => editingExistingShareScope(share.shareId, value))}</Box> {/*
when turning scope back on, change width of audience box to 310px
<Box width="160px">{renderScopeDropdown(`scope-${share.shareId}`, getScopeOption(share.scopeId), (event: React.SyntheticEvent, value: any | any[], reason: string) => editingExistingShareScope(share.shareId, value))}</Box>
*/}
</Box> </Box>
<Box pr="1rem"> <Box pr="1rem">
<Button sx={{...iconButtonSX, ...redIconButton}} onClick={() => removeShare(share.shareId)}><Icon>clear</Icon></Button> <Button sx={{...iconButtonSX, ...redIconButton}} onClick={() => removeShare(share.shareId)}><Icon>clear</Icon></Button>

View File

@ -99,9 +99,10 @@ export default function DynamicFormWidget({isEditable, widgetMetaData, widgetDat
if(newFields.length > 0) if(newFields.length > 0)
{ {
const recordOfFieldValues = widgetData.recordOfFieldValues ? new QRecord(widgetData.recordOfFieldValues) : null;
const {dynamicFormFields: newDynamicFormFields, formValidations: newFormValidations} = DynamicFormUtils.getFormData(newFields); const {dynamicFormFields: newDynamicFormFields, formValidations: newFormValidations} = DynamicFormUtils.getFormData(newFields);
const defaultDisplayValues = new Map<string,string>(); // todo - seems not right? const defaultDisplayValues = new Map<string,string>(); // todo - seems not right?
DynamicFormUtils.addPossibleValueProps(newDynamicFormFields, newFields, recordValues.tableName, null, record ? record.displayValues : defaultDisplayValues); DynamicFormUtils.addPossibleValueProps(newDynamicFormFields, newFields, recordValues.tableName, null, recordOfFieldValues ? recordOfFieldValues.displayValues : defaultDisplayValues);
setDynamicFormFields(newDynamicFormFields) setDynamicFormFields(newDynamicFormFields)
setFormValidations(newFormValidations) setFormValidations(newFormValidations)
} }
@ -226,7 +227,7 @@ export default function DynamicFormWidget({isEditable, widgetMetaData, widgetDat
{ {
const fieldNames: string[] = []; const fieldNames: string[] = [];
const fieldMap: {[name: string]: QFieldMetaData} = {}; const fieldMap: {[name: string]: QFieldMetaData} = {};
const fakeRecord = new QRecord({}); const fakeRecord = new QRecord(widgetData.recordOfFieldValues ?? {});
const mergedDynamicFormValuesIntoFieldName = widgetData.mergedDynamicFormValuesIntoFieldName; const mergedDynamicFormValuesIntoFieldName = widgetData.mergedDynamicFormValuesIntoFieldName;

View File

@ -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 {QJobStarted} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobStarted";
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord"; import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter"; import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
import {Alert, Button, CircularProgress, Icon, TablePagination} from "@mui/material"; import {Alert, Box, Button, CircularProgress, Icon, TablePagination} from "@mui/material";
import Box from "@mui/material/Box";
import Card from "@mui/material/Card"; import Card from "@mui/material/Card";
import Grid from "@mui/material/Grid"; import Grid from "@mui/material/Grid";
import Step from "@mui/material/Step"; import Step from "@mui/material/Step";
@ -48,12 +47,14 @@ import FormData from "form-data";
import {Form, Formik} from "formik"; import {Form, Formik} from "formik";
import parse from "html-react-parser"; import parse from "html-react-parser";
import QContext from "QContext"; import QContext from "QContext";
import colors from "qqq/assets/theme/base/colors";
import {QCancelButton, QSubmitButton} from "qqq/components/buttons/DefaultButtons"; import {QCancelButton, QSubmitButton} from "qqq/components/buttons/DefaultButtons";
import QDynamicForm from "qqq/components/forms/DynamicForm"; import QDynamicForm from "qqq/components/forms/DynamicForm";
import DynamicFormUtils from "qqq/components/forms/DynamicFormUtils"; import DynamicFormUtils from "qqq/components/forms/DynamicFormUtils";
import MDButton from "qqq/components/legacy/MDButton"; import MDButton from "qqq/components/legacy/MDButton";
import MDProgress from "qqq/components/legacy/MDProgress"; import MDProgress from "qqq/components/legacy/MDProgress";
import MDTypography from "qqq/components/legacy/MDTypography"; import MDTypography from "qqq/components/legacy/MDTypography";
import HelpContent from "qqq/components/misc/HelpContent";
import QRecordSidebar from "qqq/components/misc/RecordSidebar"; import QRecordSidebar from "qqq/components/misc/RecordSidebar";
import {GoogleDriveFolderPickerWrapper} from "qqq/components/processes/GoogleDriveFolderPickerWrapper"; import {GoogleDriveFolderPickerWrapper} from "qqq/components/processes/GoogleDriveFolderPickerWrapper";
import ProcessSummaryResults from "qqq/components/processes/ProcessSummaryResults"; import ProcessSummaryResults from "qqq/components/processes/ProcessSummaryResults";
@ -87,6 +88,14 @@ const INITIAL_RETRY_MILLIS = 1_500;
const RETRY_MAX_MILLIS = 12_000; const RETRY_MAX_MILLIS = 12_000;
const BACKOFF_AMOUNT = 1.5; 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 function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, isReport, recordIds, closeModalHandler, forceReInit, overrideLabel}: Props): JSX.Element
{ {
const processNameParam = useParams().processName; 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 ( return (
<> <>
{ {
@ -460,6 +478,16 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
</MDTypography> </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 // // 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"]; 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 ( return (
<div key={index}> <div key={index}>
{ {
@ -567,9 +612,22 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
) )
} }
{ {
component.type === QComponentType.EDIT_FORM && ( component.type === QComponentType.EDIT_FORM &&
<QDynamicForm formData={formData} helpRoles={helpRoles} helpContentKeyPrefix={`process:${processName};`} /> <>
) {
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 && ( component.type === QComponentType.VIEW_FORM && step.viewFields && (
@ -1026,6 +1084,26 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
setProcessValues(qJobComplete.values); setProcessValues(qJobComplete.values);
setQJobRunning(null); 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) if (activeStep && activeStep.recordListFields)
{ {
setNeedRecords(true); setNeedRecords(true);
@ -1385,89 +1463,98 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
> >
{({ {({
values, errors, touched, isSubmitting, setFieldValue, values, errors, touched, isSubmitting, setFieldValue,
}) => ( }) =>
<Form style={formStyles} id={formId} autoComplete="off"> {
<Card sx={mainCardStyles}> ///////////////////////////////////////////////////////////////////
{ // once we're in the formik form, use its setFieldValue function //
!isWidget && ( // over top of the default one we created globally //
<Box mx={2} mt={-3}> ///////////////////////////////////////////////////////////////////
<Stepper activeStep={activeStepIndex} alternativeLabel> formikSetFieldValueFunction = setFieldValue;
{steps.map((step) => (
<Step key={step.name}>
<StepLabel>{step.label}</StepLabel>
</Step>
))}
</Stepper>
</Box>
)
}
<Box p={3}> return (
<Box pb={isWidget ? 6 : "initial"}> <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 ** ** step content - e.g., the appropriate form or other screen for the step **
***************************************************************************/} ***************************************************************************/}
{getDynamicStepContent( {getDynamicStepContent(
activeStepIndex, activeStepIndex,
activeStep, activeStep,
{ {
values, values,
touched, touched,
formFields, formFields,
errors, errors,
}, },
processError, processError,
processValues, processValues,
recordConfig, recordConfig,
setFieldValue, setFieldValue,
)} )}
{/******************************** {/********************************
** back &| next/submit buttons ** ** 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"}> <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 ? ( {true || activeStepIndex === 0 ? (
<Box /> <Box />
) : ( ) : (
<MDButton variant="gradient" color="light" onClick={handleBack}>back</MDButton> <MDButton variant="gradient" color="light" onClick={handleBack}>back</MDButton>
)} )}
{processError || qJobRunning || !activeStep ? ( {processError || qJobRunning || !activeStep ? (
<Box /> <Box />
) : ( ) : (
<> <>
{formError && ( {formError && (
<MDTypography component="div" variant="caption" color="error" fontWeight="regular" align="right" fullWidth> <MDTypography component="div" variant="caption" color="error" fontWeight="regular" align="right" fullWidth>
{formError} {formError}
</MDTypography> </MDTypography>
)} )}
{ {
noMoreSteps && <QCancelButton noMoreSteps && <QCancelButton
onClickHandler={handleCancelClicked} onClickHandler={handleCancelClicked}
label={isModal ? "Close" : "Return"} label={isModal ? "Close" : "Return"}
iconName={isModal ? "cancel" : "arrow_back"} iconName={isModal ? "cancel" : "arrow_back"}
disabled={isSubmitting} /> disabled={isSubmitting} />
} }
{ {
!noMoreSteps && ( !noMoreSteps && (
<Box component="div" py={3}> <Box component="div" py={3}>
<Grid container justifyContent="flex-end" spacing={3}> <Grid container justifyContent="flex-end" spacing={3}>
{ {
!isWidget && ( !isWidget && (
<QCancelButton onClickHandler={handleCancelClicked} disabled={isSubmitting} /> <QCancelButton onClickHandler={handleCancelClicked} disabled={isSubmitting} />
) )
} }
<QSubmitButton label={nextButtonLabel} iconName={nextButtonIcon} disabled={isSubmitting} /> <QSubmitButton label={nextButtonLabel} iconName={nextButtonIcon} disabled={isSubmitting} />
</Grid> </Grid>
</Box> </Box>
) )
} }
</> </>
)} )}
</Box>
</Box> </Box>
</Box> </Box>
</Box> </Card>
</Card> </Form>
</Form> )
)} }}
</Formik> </Formik>
); );

View File

@ -867,7 +867,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, initialQueryFilter, init
for (let i = 0; i < queryFilter?.orderBys?.length; i++) for (let i = 0; i < queryFilter?.orderBys?.length; i++)
{ {
const fieldName = queryFilter.orderBys[i].fieldName; const fieldName = queryFilter.orderBys[i].fieldName;
if (fieldName.indexOf(".") > -1) if (fieldName != null && fieldName.indexOf(".") > -1)
{ {
const joinTableName = fieldName.replaceAll(/\..*/g, ""); const joinTableName = fieldName.replaceAll(/\..*/g, "");
if (!vjtToUse.has(joinTableName)) if (!vjtToUse.has(joinTableName))
@ -900,6 +900,26 @@ const RecordQuery = forwardRef(({table, usage, isModal, initialQueryFilter, init
return; return;
} }
/////////////////////////////////////////////////////////////////////////////////////////////////
// if any values in the query are of type "FilterVariableExpression", display an error showing //
// that a backend query cannot be made because of missing values for that expression //
/////////////////////////////////////////////////////////////////////////////////////////////////
setWarningAlert(null);
for (var i = 0; i < queryFilter?.criteria?.length; i++)
{
for (var j = 0; j < queryFilter?.criteria[i]?.values?.length; j++)
{
const value = queryFilter.criteria[i].values[j];
if (value?.type == "FilterVariableExpression")
{
setWarningAlert("Cannot perform query because of a missing value for a variable.");
setLoading(false);
setRows([]);
return;
}
}
}
recordAnalytics({category: "tableEvents", action: "query", label: tableMetaData.label}); recordAnalytics({category: "tableEvents", action: "query", label: tableMetaData.label});
console.log(`In updateTable for ${reason} ${JSON.stringify(queryFilter)}`); console.log(`In updateTable for ${reason} ${JSON.stringify(queryFilter)}`);
@ -2888,6 +2908,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, initialQueryFilter, init
filterPanel: filterPanel:
{ {
tableMetaData: tableMetaData, tableMetaData: tableMetaData,
queryScreenUsage: usage,
metaData: metaData, metaData: metaData,
queryFilter: queryFilter, queryFilter: queryFilter,
updateFilter: doSetQueryFilter, updateFilter: doSetQueryFilter,

View File

@ -74,12 +74,14 @@ const qController = Client.getInstance();
interface Props interface Props
{ {
table?: QTableMetaData; table?: QTableMetaData;
record?: QRecord;
launchProcess?: QProcessMetaData; launchProcess?: QProcessMetaData;
} }
RecordView.defaultProps = RecordView.defaultProps =
{ {
table: null, table: null,
record: null,
launchProcess: 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. ** Record View Screen component.
*******************************************************************************/ *******************************************************************************/
function RecordView({table, launchProcess}: Props): JSX.Element function RecordView({table, record: overrideRecord, launchProcess}: Props): JSX.Element
{ {
const {id} = useParams(); const {id} = useParams();
@ -147,7 +178,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element
const [adornmentFieldsMap, setAdornmentFieldsMap] = useState(new Map<string, boolean>); const [adornmentFieldsMap, setAdornmentFieldsMap] = useState(new Map<string, boolean>);
const [deleteConfirmationOpen, setDeleteConfirmationOpen] = useState(false); const [deleteConfirmationOpen, setDeleteConfirmationOpen] = useState(false);
const [metaData, setMetaData] = useState(null as QInstance); 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 [tableSections, setTableSections] = useState([] as QTableSection[]);
const [t1Section, setT1Section] = useState(null as QTableSection); const [t1Section, setT1Section] = useState(null as QTableSection);
const [t1SectionName, setT1SectionName] = useState(null as string); const [t1SectionName, setT1SectionName] = useState(null as string);
@ -381,31 +412,6 @@ function RecordView({table, launchProcess}: Props): JSX.Element
reload(); reload();
}, [location.pathname, location.hash]); }, [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 ** 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; let record: QRecord;
try 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); setRecord(record);
recordAnalytics({category: "tableEvents", action: "view", label: tableMetaData?.label + " / " + record?.recordLabel}); recordAnalytics({category: "tableEvents", action: "view", label: tableMetaData?.label + " / " + record?.recordLabel});
} }

View 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>);
}
}

View File

@ -693,6 +693,11 @@ input[type="search"]::-webkit-search-results-decoration
padding: 24px; padding: 24px;
} }
.entityForm .widget
{
padding: 24px;
}
.recordView .widget .recordGridWidget .recordView .widget .recordGridWidget
{ {
margin: -8px; margin: -8px;

View File

@ -109,6 +109,8 @@ class FilterUtils
let [field, fieldTable] = TableUtils.getFieldAndTable(tableMetaData, criteria.fieldName); let [field, fieldTable] = TableUtils.getFieldAndTable(tableMetaData, criteria.fieldName);
let values = criteria.values; let values = criteria.values;
let hasFilterVariable = false;
if (field.possibleValueSourceName) if (field.possibleValueSourceName)
{ {
////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////
@ -122,7 +124,17 @@ class FilterUtils
////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////
if (values && values.length > 0 && values[0] !== null && values[0] !== undefined && values[0] !== "") if (values && values.length > 0 && values[0] !== null && values[0] !== undefined && values[0] !== "")
{ {
values = await qController.possibleValues(fieldTable.name, null, field.name, "", values); ////////////////////////////////////////////////////////////////////////
// do not do this lookup if the field is a filter variable expression //
////////////////////////////////////////////////////////////////////////
if (values[0].type && values[0].type == "FilterVariableExpression")
{
hasFilterVariable = true;
}
else
{
values = await qController.possibleValues(fieldTable.name, null, field.name, "", values);
}
} }
//////////////////////////////////////////// ////////////////////////////////////////////
@ -233,6 +245,10 @@ class FilterUtils
{ {
return (new ThisOrLastPeriodExpression(value)); return (new ThisOrLastPeriodExpression(value));
} }
else if (value.type == "FilterVariableExpression")
{
return (new FilterVariableExpression(value));
}
} }
return (null); return (null);

View File

@ -133,6 +133,11 @@ class TableUtils
*******************************************************************************/ *******************************************************************************/
public static getFieldAndTable(tableMetaData: QTableMetaData, fieldName: string): [QFieldMetaData, QTableMetaData] public static getFieldAndTable(tableMetaData: QTableMetaData, fieldName: string): [QFieldMetaData, QTableMetaData]
{ {
if(!fieldName)
{
return [null, null];
}
if (fieldName.indexOf(".") > -1) if (fieldName.indexOf(".") > -1)
{ {
const nameParts = fieldName.split(".", 2); const nameParts = fieldName.split(".", 2);

View File

@ -335,7 +335,7 @@ public class QSeleniumLib
return; 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 + "]"); LOG.debug("Found non-existence of element(s) matching selector [" + cssSelector + "] containing text [" + textContains + "]");
return; return;
@ -345,7 +345,7 @@ public class QSeleniumLib
} }
while(start + (1000 * WAIT_SECONDS) > System.currentTimeMillis()); 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.");
} }