Compare commits

..

26 Commits

Author SHA1 Message Date
d750ef0930 CE-1240: made link font slightly larger 2024-05-06 15:30:27 -05:00
267ead925b CE-1240: added support for table link 2024-05-06 11:58:57 -05:00
f925ad9116 CE-1240: updated composite widget to have flex column ability, support for 'multi table' widget, 2024-05-03 20:26:36 -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 939 additions and 18486 deletions

18673
package-lock.json generated

File diff suppressed because it is too large Load Diff

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

@ -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

@ -22,16 +22,16 @@
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData"; import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
import {Box, Skeleton} from "@mui/material"; import {Box, Skeleton} from "@mui/material";
import React from "react";
import {BlockData} from "qqq/components/widgets/blocks/BlockModels"; import {BlockData} from "qqq/components/widgets/blocks/BlockModels";
import WidgetBlock from "qqq/components/widgets/WidgetBlock"; import WidgetBlock from "qqq/components/widgets/WidgetBlock";
import React from "react";
interface CompositeData interface CompositeData
{ {
blocks: BlockData[]; blocks: BlockData[];
styleOverrides?: any; styleOverrides?: any;
layout?: string layout?: string;
} }
@ -57,7 +57,14 @@ export default function CompositeWidget({widgetMetaData, data}: CompositeWidgetP
//////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////
let layout = data?.layout; let layout = data?.layout;
let boxStyle: any = {}; let boxStyle: any = {};
if (layout == "FLEX_ROW_WRAPPED") if (layout == "FLEX_COLUMN")
{
boxStyle.display = "flex";
boxStyle.flexDirection = "column";
boxStyle.flexWrap = "wrap";
boxStyle.gap = "0.5rem";
}
else if (layout == "FLEX_ROW_WRAPPED")
{ {
boxStyle.display = "flex"; boxStyle.display = "flex";
boxStyle.flexDirection = "row"; boxStyle.flexDirection = "row";
@ -68,7 +75,7 @@ export default function CompositeWidget({widgetMetaData, data}: CompositeWidgetP
{ {
boxStyle.display = "flex"; boxStyle.display = "flex";
boxStyle.flexDirection = "row"; boxStyle.flexDirection = "row";
boxStyle.justifyContent = "space-between" boxStyle.justifyContent = "space-between";
boxStyle.gap = "0.25rem"; boxStyle.gap = "0.25rem";
} }
else if (layout == "TABLE_SUB_ROW_DETAILS") else if (layout == "TABLE_SUB_ROW_DETAILS")

View File

@ -50,7 +50,7 @@ import USMapWidget from "qqq/components/widgets/misc/USMapWidget";
import ParentWidget from "qqq/components/widgets/ParentWidget"; import ParentWidget from "qqq/components/widgets/ParentWidget";
import MultiStatisticsCard from "qqq/components/widgets/statistics/MultiStatisticsCard"; import MultiStatisticsCard from "qqq/components/widgets/statistics/MultiStatisticsCard";
import StatisticsCard from "qqq/components/widgets/statistics/StatisticsCard"; import StatisticsCard from "qqq/components/widgets/statistics/StatisticsCard";
import Widget, {HeaderIcon, LabelComponent, WIDGET_DROPDOWN_SELECTION_LOCAL_STORAGE_KEY_ROOT} from "qqq/components/widgets/Widget"; import Widget, {HeaderIcon, LabelComponent, WIDGET_DROPDOWN_SELECTION_LOCAL_STORAGE_KEY_ROOT, WidgetData} from "qqq/components/widgets/Widget";
import WidgetBlock from "qqq/components/widgets/WidgetBlock"; import WidgetBlock from "qqq/components/widgets/WidgetBlock";
import ProcessRun from "qqq/pages/processes/ProcessRun"; import ProcessRun from "qqq/pages/processes/ProcessRun";
import Client from "qqq/utils/qqq/Client"; import Client from "qqq/utils/qqq/Client";
@ -258,11 +258,11 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
** helper function, to convert values from a QRecord values map to a regular old ** helper function, to convert values from a QRecord values map to a regular old
** js object ** js object
*******************************************************************************/ *******************************************************************************/
function convertQRecordValuesFromMapToObject(record: QRecord): {[name: string]: any} function convertQRecordValuesFromMapToObject(record: QRecord): { [name: string]: any }
{ {
const rs: {[name: string]: any} = {}; const rs: { [name: string]: any } = {};
if(record && record.values) if (record && record.values)
{ {
record.values.forEach((value, key) => rs[key] = value); record.values.forEach((value, key) => rs[key] = value);
} }
@ -293,7 +293,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
} }
return ( return (
<Box key={`${widgetMetaData.name}-${i}`} sx={{alignItems: "stretch", flexGrow: 1, display: "flex", marginTop: "0px", paddingTop: "0px", width: "100%", height: "100%"}}> <Box key={`${widgetMetaData.name}-${i}`} sx={{alignItems: "stretch", flexGrow: 1, display: "flex", marginTop: "0px", paddingTop: "0px", width: "100%", height: "100%", flexDirection: widgetMetaData.type == "multiTable" ? "column" : "row"}}>
{ {
haveLoadedParams && widgetMetaData.type === "parentWidget" && ( haveLoadedParams && widgetMetaData.type === "parentWidget" && (
<ParentWidget <ParentWidget
@ -343,6 +343,20 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
/> />
) )
} }
{
widgetMetaData.type === "multiTable" && (
widgetData[i]?.tableDataList?.map((tableData: WidgetData, index: number) =>
<Box pb={3} key={`${widgetMetaData.type}-${index}`}>
<TableWidget
widgetMetaData={widgetMetaData}
widgetData={tableData}
reloadWidgetCallback={(data) => reloadWidget(i, data)}
isChild={areChildren}
/>
</Box>
)
)
}
{ {
widgetMetaData.type === "stackedBarChart" && ( widgetMetaData.type === "stackedBarChart" && (
<Widget <Widget
@ -587,14 +601,16 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
widgetMetaData.type === "reportSetup" && ( widgetMetaData.type === "reportSetup" && (
widgetData && widgetData[i] && widgetData[i].queryParams && widgetData && widgetData[i] && widgetData[i].queryParams &&
<ReportSetupWidget isEditable={false} widgetMetaData={widgetMetaData} recordValues={convertQRecordValuesFromMapToObject(record)} onSaveCallback={() => <ReportSetupWidget isEditable={false} widgetMetaData={widgetMetaData} recordValues={convertQRecordValuesFromMapToObject(record)} onSaveCallback={() =>
{}} /> {
}} />
) )
} }
{ {
widgetMetaData.type === "pivotTableSetup" && ( widgetMetaData.type === "pivotTableSetup" && (
widgetData && widgetData[i] && widgetData[i].queryParams && widgetData && widgetData[i] && widgetData[i].queryParams &&
<PivotTableSetupWidget isEditable={false} widgetMetaData={widgetMetaData} recordValues={convertQRecordValuesFromMapToObject(record)} onSaveCallback={() => <PivotTableSetupWidget isEditable={false} widgetMetaData={widgetMetaData} recordValues={convertQRecordValuesFromMapToObject(record)} onSaveCallback={() =>
{}} /> {
}} />
) )
} }
{ {

View File

@ -21,14 +21,16 @@
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData"; import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import Icon from "@mui/material/Icon"; import Icon from "@mui/material/Icon";
import Tooltip from "@mui/material/Tooltip/Tooltip"; import Tooltip from "@mui/material/Tooltip/Tooltip";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import React from "react";
import colors from "qqq/assets/theme/base/colors"; import colors from "qqq/assets/theme/base/colors";
import {WidgetData} from "qqq/components/widgets/Widget"; import {WidgetData} from "qqq/components/widgets/Widget";
import ValueUtils from "qqq/utils/qqq/ValueUtils"; import ValueUtils from "qqq/utils/qqq/ValueUtils";
import React from "react";
import {Link} from "react-router-dom";
/******************************************************************************* /*******************************************************************************
** Utility class used by Widgets ** Utility class used by Widgets
@ -51,6 +53,17 @@ export class WidgetUtils
}; };
/*******************************************************************************
**
*******************************************************************************/
public static generateLabelLink = (linkText: string, linkURL: string): JSX.Element =>
{
return (<Box key={1} fontSize="1rem" pl={1} display="inline" position="relative">
(<Link to={linkURL}>{linkText}</Link>)
</Box>);
};
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
@ -97,4 +110,4 @@ export class WidgetUtils
return (fileName); return (fileName);
}; };
} }

View File

@ -41,7 +41,7 @@ export default function NumberIconBadgeBlock({widgetMetaData, data}: StandardBlo
{ {
data.values.iconName && data.values.iconName &&
<BlockElementWrapper metaData={widgetMetaData} data={data} slot="icon"> <BlockElementWrapper metaData={widgetMetaData} data={data} slot="icon">
<Icon style={{color: data.styles.color, fontSize: "1rem", position: "relative", top: "3px"}}>{data.values.iconName}</Icon> <Icon style={{color: data.styles.color, fontSize: "1rem", marginLeft: "2px", position: "relative", top: "4px"}}>{data.values.iconName}</Icon>
</BlockElementWrapper> </BlockElementWrapper>
} }
</div>); </div>);

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

@ -30,8 +30,6 @@ import TableContainer from "@mui/material/TableContainer";
import TableRow from "@mui/material/TableRow"; import TableRow from "@mui/material/TableRow";
import Tooltip from "@mui/material/Tooltip"; import Tooltip from "@mui/material/Tooltip";
import parse from "html-react-parser"; import parse from "html-react-parser";
import React, {useEffect, useMemo, useState} from "react";
import {useAsyncDebounce, useExpanded, useGlobalFilter, usePagination, useSortBy, useTable} from "react-table";
import colors from "qqq/assets/theme/base/colors"; import colors from "qqq/assets/theme/base/colors";
import MDInput from "qqq/components/legacy/MDInput"; import MDInput from "qqq/components/legacy/MDInput";
import MDPagination from "qqq/components/legacy/MDPagination"; import MDPagination from "qqq/components/legacy/MDPagination";
@ -43,6 +41,8 @@ import DefaultCell from "qqq/components/widgets/tables/cells/DefaultCell";
import ImageCell from "qqq/components/widgets/tables/cells/ImageCell"; import ImageCell from "qqq/components/widgets/tables/cells/ImageCell";
import {TableDataInput} from "qqq/components/widgets/tables/TableCard"; import {TableDataInput} from "qqq/components/widgets/tables/TableCard";
import WidgetBlock from "qqq/components/widgets/WidgetBlock"; import WidgetBlock from "qqq/components/widgets/WidgetBlock";
import React, {useEffect, useMemo, useState} from "react";
import {useAsyncDebounce, useExpanded, useGlobalFilter, usePagination, useSortBy, useTable} from "react-table";
interface Props interface Props
{ {
@ -106,17 +106,17 @@ function DataTable({
entries = entriesPerPageOptions ? entriesPerPageOptions : ["10", "25", "50", "100"]; entries = entriesPerPageOptions ? entriesPerPageOptions : ["10", "25", "50", "100"];
let widths = []; let widths = [];
for(let i = 0; i<table.columns.length; i++) for (let i = 0; i < table.columns.length; i++)
{ {
const column = table.columns[i]; const column = table.columns[i];
if(column.type !== "hidden") if (column.type !== "hidden")
{ {
widths.push(table.columns[i].width ?? "1fr"); widths.push(table.columns[i].width ?? "1fr");
} }
} }
let showExpandColumn = false; let showExpandColumn = false;
if(table.rows) if (table.rows)
{ {
for (let i = 0; i < table.rows.length; i++) for (let i = 0; i < table.rows.length; i++)
{ {
@ -129,7 +129,7 @@ function DataTable({
} }
const columnsToMemo = [...table.columns]; const columnsToMemo = [...table.columns];
if(showExpandColumn) if (showExpandColumn)
{ {
widths.push("60px"); widths.push("60px");
columnsToMemo.push( columnsToMemo.push(
@ -173,11 +173,11 @@ function DataTable({
); );
} }
if(table.columnHeaderTooltips) if (table.columnHeaderTooltips)
{ {
for (let column of columnsToMemo) for (let column of columnsToMemo)
{ {
if(table.columnHeaderTooltips[column.accessor]) if (table.columnHeaderTooltips[column.accessor])
{ {
column.tooltip = table.columnHeaderTooltips[column.accessor]; column.tooltip = table.columnHeaderTooltips[column.accessor];
} }
@ -297,7 +297,7 @@ function DataTable({
} }
let visibleFooterRows = 1; let visibleFooterRows = 1;
if(expanded && expanded[`${table.rows.length-1}`]) if (expanded && expanded[`${table.rows.length - 1}`])
{ {
////////////////////////////////////////////////// //////////////////////////////////////////////////
// todo - should count how many are expanded... // // todo - should count how many are expanded... //
@ -308,7 +308,7 @@ function DataTable({
function getTable(includeHead: boolean, rows: any, isFooter: boolean) function getTable(includeHead: boolean, rows: any, isFooter: boolean)
{ {
let boxStyle = {}; let boxStyle = {};
if(fixedStickyLastRow) if (fixedStickyLastRow)
{ {
boxStyle = isFooter boxStyle = isFooter
? {borderTop: `0.0625rem solid ${colors.grayLines.main};`, backgroundColor: "#EEEEEE"} ? {borderTop: `0.0625rem solid ${colors.grayLines.main};`, backgroundColor: "#EEEEEE"}
@ -316,7 +316,7 @@ function DataTable({
} }
let innerBoxStyle = {}; let innerBoxStyle = {};
if(fixedStickyLastRow && isFooter) if (fixedStickyLastRow && isFooter)
{ {
innerBoxStyle = {overflowY: "auto", scrollbarGutter: "stable"}; innerBoxStyle = {overflowY: "auto", scrollbarGutter: "stable"};
} }
@ -327,7 +327,7 @@ function DataTable({
includeHead && ( includeHead && (
<Box component="thead" sx={{position: "sticky", top: 0, background: "white", zIndex: 10}}> <Box component="thead" sx={{position: "sticky", top: 0, background: "white", zIndex: 10}}>
{headerGroups.map((headerGroup: any, i: number) => ( {headerGroups.map((headerGroup: any, i: number) => (
<TableRow key={i} {...headerGroup.getHeaderGroupProps()} sx={{display: "grid", gridTemplateColumns: gridTemplateColumns}}> <TableRow key={i} {...headerGroup.getHeaderGroupProps()} sx={{display: "grid", alignItems: "flex-end", gridTemplateColumns: gridTemplateColumns}}>
{headerGroup.headers.map((column: any) => ( {headerGroup.headers.map((column: any) => (
column.type !== "hidden" && ( column.type !== "hidden" && (
<DataTableHeadCell <DataTableHeadCell
@ -356,10 +356,10 @@ function DataTable({
////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////
// don't do an end-border on nested rows - unless they're the last one in a set // // don't do an end-border on nested rows - unless they're the last one in a set //
////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////
if(row.depth > 0) if (row.depth > 0)
{ {
overrideNoEndBorder = true; overrideNoEndBorder = true;
if(key + 1 < rows.length && rows[key + 1].depth == 0) if (key + 1 < rows.length && rows[key + 1].depth == 0)
{ {
overrideNoEndBorder = false; overrideNoEndBorder = false;
} }
@ -368,17 +368,17 @@ function DataTable({
/////////////////////////////////////// ///////////////////////////////////////
// don't do end-border on the footer // // don't do end-border on the footer //
/////////////////////////////////////// ///////////////////////////////////////
if(isFooter) if (isFooter)
{ {
overrideNoEndBorder = true; overrideNoEndBorder = true;
} }
let background = "initial"; let background = "initial";
if(isFooter) if (isFooter)
{ {
background = "#EEEEEE"; background = "#EEEEEE";
} }
else if(row.depth > 0 || row.isExpanded) else if (row.depth > 0 || row.isExpanded)
{ {
background = "#FAFAFA"; background = "#FAFAFA";
} }
@ -453,7 +453,7 @@ function DataTable({
</TableBody> </TableBody>
</Table> </Table>
</Box></Box> </Box></Box>;
} }
return ( return (

View File

@ -28,13 +28,13 @@ import TableBody from "@mui/material/TableBody";
import TableContainer from "@mui/material/TableContainer"; import TableContainer from "@mui/material/TableContainer";
import TableRow from "@mui/material/TableRow"; import TableRow from "@mui/material/TableRow";
import parse from "html-react-parser"; import parse from "html-react-parser";
import React, {useEffect, useState} from "react";
import MDTypography from "qqq/components/legacy/MDTypography"; import MDTypography from "qqq/components/legacy/MDTypography";
import DataTableBodyCell from "qqq/components/widgets/tables/cells/DataTableBodyCell"; import DataTableBodyCell from "qqq/components/widgets/tables/cells/DataTableBodyCell";
import DataTableHeadCell from "qqq/components/widgets/tables/cells/DataTableHeadCell"; import DataTableHeadCell from "qqq/components/widgets/tables/cells/DataTableHeadCell";
import DefaultCell from "qqq/components/widgets/tables/cells/DefaultCell"; import DefaultCell from "qqq/components/widgets/tables/cells/DefaultCell";
import DataTable from "qqq/components/widgets/tables/DataTable"; import DataTable from "qqq/components/widgets/tables/DataTable";
import Client from "qqq/utils/qqq/Client"; import Client from "qqq/utils/qqq/Client";
import React, {useEffect, useState} from "react";
////////////////////////////////////// //////////////////////////////////////
@ -43,7 +43,7 @@ import Client from "qqq/utils/qqq/Client";
export interface TableDataInput export interface TableDataInput
{ {
columns: { [key: string]: any }[]; columns: { [key: string]: any }[];
columnHeaderTooltips?: { [columnName: string]: string | JSX.Element } columnHeaderTooltips?: { [columnName: string]: string | JSX.Element };
rows: { [key: string]: any }[]; rows: { [key: string]: any }[];
} }
@ -63,6 +63,7 @@ interface Props
} }
const qController = Client.getInstance(); const qController = Client.getInstance();
function TableCard({noRowsFoundHTML, data, rowsPerPage, hidePaginationDropdown, fixedStickyLastRow, fixedHeight, widgetMetaData}: Props): JSX.Element function TableCard({noRowsFoundHTML, data, rowsPerPage, hidePaginationDropdown, fixedStickyLastRow, fixedHeight, widgetMetaData}: Props): JSX.Element
{ {
const [qInstance, setQInstance] = useState(null as QInstance); const [qInstance, setQInstance] = useState(null as QInstance);
@ -108,7 +109,7 @@ function TableCard({noRowsFoundHTML, data, rowsPerPage, hidePaginationDropdown,
<TableContainer sx={{boxShadow: "none"}}> <TableContainer sx={{boxShadow: "none"}}>
<Table> <Table>
<Box component="thead"> <Box component="thead">
<TableRow key="header"> <TableRow sx={{alignItems: "flex-end"}} key="header">
{Array(8).fill(0).map((_, i) => {Array(8).fill(0).map((_, i) =>
<DataTableHeadCell key={`head-${i}`} sorted={false} width="auto" align="center"> <DataTableHeadCell key={`head-${i}`} sorted={false} width="auto" align="center">
<Skeleton width="100%" /> <Skeleton width="100%" />

View File

@ -23,7 +23,6 @@
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData"; import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
// @ts-ignore // @ts-ignore
import {htmlToText} from "html-to-text"; import {htmlToText} from "html-to-text";
import React, {useContext, useEffect, useState} from "react";
import QContext from "QContext"; import QContext from "QContext";
import HelpContent, {hasHelpContent} from "qqq/components/misc/HelpContent"; import HelpContent, {hasHelpContent} from "qqq/components/misc/HelpContent";
import TableCard from "qqq/components/widgets/tables/TableCard"; import TableCard from "qqq/components/widgets/tables/TableCard";
@ -31,6 +30,7 @@ import Widget, {WidgetData} from "qqq/components/widgets/Widget";
import {WidgetUtils} from "qqq/components/widgets/WidgetUtils"; import {WidgetUtils} from "qqq/components/widgets/WidgetUtils";
import HtmlUtils from "qqq/utils/HtmlUtils"; import HtmlUtils from "qqq/utils/HtmlUtils";
import ValueUtils from "qqq/utils/qqq/ValueUtils"; import ValueUtils from "qqq/utils/qqq/ValueUtils";
import React, {useContext, useEffect, useState} from "react";
interface Props interface Props
{ {
@ -40,8 +40,7 @@ interface Props
isChild?: boolean; isChild?: boolean;
} }
TableWidget.defaultProps = { TableWidget.defaultProps = {};
};
function TableWidget(props: Props): JSX.Element function TableWidget(props: Props): JSX.Element
{ {
@ -86,7 +85,7 @@ function TableWidget(props: Props): JSX.Element
const cell = rows[i][columns[j].accessor]; const cell = rows[i][columns[j].accessor];
let text = cell; let text = cell;
if(columns[j].type != "default") if (columns[j].type != "default")
{ {
text = htmlToText(cell, text = htmlToText(cell,
{ {
@ -105,7 +104,7 @@ function TableWidget(props: Props): JSX.Element
setCsv(csv); setCsv(csv);
const fileName = WidgetUtils.makeExportFileName(props.widgetData, props.widgetMetaData); const fileName = WidgetUtils.makeExportFileName(props.widgetData, props.widgetMetaData);
setFileName(fileName) setFileName(fileName);
console.log(`useEffect, setting fileName ${fileName}`); console.log(`useEffect, setting fileName ${fileName}`);
} }
@ -114,24 +113,28 @@ function TableWidget(props: Props): JSX.Element
const onExportClick = () => const onExportClick = () =>
{ {
if(props.widgetData?.csvData) if (props.widgetData?.csvData)
{ {
const csv = WidgetUtils.widgetCsvDataToString(props.widgetData); const csv = WidgetUtils.widgetCsvDataToString(props.widgetData);
const fileName = WidgetUtils.makeExportFileName(props.widgetData, props.widgetMetaData); const fileName = WidgetUtils.makeExportFileName(props.widgetData, props.widgetMetaData);
HtmlUtils.download(fileName, csv); HtmlUtils.download(fileName, csv);
} }
else if(csv) else if (csv)
{ {
HtmlUtils.download(fileName, csv); HtmlUtils.download(fileName, csv);
} }
else else
{ {
alert("There is no data available to export.") alert("There is no data available to export.");
} }
} };
const labelAdditionalElementsLeft: JSX.Element[] = []; const labelAdditionalElementsLeft: JSX.Element[] = [];
if(props.widgetMetaData?.showExportButton) if (props.widgetData?.linkText && props.widgetData?.linkURL)
{
labelAdditionalElementsLeft.push(WidgetUtils.generateLabelLink(props.widgetData?.linkText, props.widgetData?.linkURL));
}
if (props.widgetMetaData?.showExportButton)
{ {
labelAdditionalElementsLeft.push(WidgetUtils.generateExportButton(onExportClick)); labelAdditionalElementsLeft.push(WidgetUtils.generateExportButton(onExportClick));
} }
@ -139,14 +142,14 @@ function TableWidget(props: Props): JSX.Element
////////////////////////////////////////////////////// //////////////////////////////////////////////////////
// look for column-header tooltips from helpContent // // look for column-header tooltips from helpContent //
////////////////////////////////////////////////////// //////////////////////////////////////////////////////
const columnHeaderTooltips: {[columnName: string]: JSX.Element} = {} const columnHeaderTooltips: { [columnName: string]: JSX.Element } = {};
for (let column of props.widgetData?.columns ?? []) for (let column of props.widgetData?.columns ?? [])
{ {
const helpRoles = ["ALL_SCREENS"] const helpRoles = ["ALL_SCREENS"];
const slotName = `columnHeader=${column.accessor}`; const slotName = `columnHeader=${column.accessor}`;
const showHelp = helpHelpActive || hasHelpContent(props.widgetMetaData?.helpContent?.get(slotName), helpRoles); const showHelp = helpHelpActive || hasHelpContent(props.widgetMetaData?.helpContent?.get(slotName), helpRoles);
if(showHelp) if (showHelp)
{ {
const formattedHelpContent = <HelpContent helpContents={props.widgetMetaData?.helpContent?.get(slotName)} roles={helpRoles} helpContentKey={`widget:${props.widgetMetaData?.name};slot:${slotName}`} />; const formattedHelpContent = <HelpContent helpContents={props.widgetMetaData?.helpContent?.get(slotName)} roles={helpRoles} helpContentKey={`widget:${props.widgetMetaData?.name};slot:${slotName}`} />;
columnHeaderTooltips[column.accessor] = formattedHelpContent; columnHeaderTooltips[column.accessor] = formattedHelpContent;

View File

@ -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

@ -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);