Compare commits

...

11 Commits

9 changed files with 18917 additions and 473 deletions

18466
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -6,7 +6,7 @@
"@auth0/auth0-react": "1.10.2", "@auth0/auth0-react": "1.10.2",
"@emotion/react": "11.7.1", "@emotion/react": "11.7.1",
"@emotion/styled": "11.6.0", "@emotion/styled": "11.6.0",
"@kingsrook/qqq-frontend-core": "1.0.96", "@kingsrook/qqq-frontend-core": "1.0.97",
"@mui/icons-material": "5.4.1", "@mui/icons-material": "5.4.1",
"@mui/material": "5.11.1", "@mui/material": "5.11.1",
"@mui/styles": "5.11.1", "@mui/styles": "5.11.1",

View File

@ -664,10 +664,16 @@ export default function App()
const [dotMenuOpen, setDotMenuOpen] = useState(false); const [dotMenuOpen, setDotMenuOpen] = useState(false);
const [keyboardHelpOpen, setKeyboardHelpOpen] = useState(false); const [keyboardHelpOpen, setKeyboardHelpOpen] = useState(false);
const [helpHelpActive] = useState(queryParams.has("helpHelp")); const [helpHelpActive] = useState(queryParams.has("helpHelp"));
const [userId, setUserId] = useState(user?.email);
useEffect(() =>
{
setUserId(user?.email)
}, [user]);
const [googleAnalyticsUtils] = useState(new GoogleAnalyticsUtils()); const [googleAnalyticsUtils] = useState(new GoogleAnalyticsUtils());
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
@ -689,6 +695,7 @@ export default function App()
dotMenuOpen: dotMenuOpen, dotMenuOpen: dotMenuOpen,
keyboardHelpOpen: keyboardHelpOpen, keyboardHelpOpen: keyboardHelpOpen,
helpHelpActive: helpHelpActive, helpHelpActive: helpHelpActive,
userId: userId,
setPageHeader: (header: string | JSX.Element) => setPageHeader(header), setPageHeader: (header: string | JSX.Element) => setPageHeader(header),
setAccentColor: (accentColor: string) => setAccentColor(accentColor), setAccentColor: (accentColor: string) => setAccentColor(accentColor),
setAccentColorLight: (accentColorLight: string) => setAccentColorLight(accentColorLight), setAccentColorLight: (accentColorLight: string) => setAccentColorLight(accentColorLight),

View File

@ -59,6 +59,7 @@ interface QContext
pathToLabelMap?: {[path: string]: string}; pathToLabelMap?: {[path: string]: string};
branding?: QBrandingMetaData; branding?: QBrandingMetaData;
helpHelpActive?: boolean; helpHelpActive?: boolean;
userId?: string;
} }
const defaultState = { const defaultState = {

View File

@ -28,16 +28,17 @@ import Box from "@mui/material/Box";
import Switch from "@mui/material/Switch"; import Switch from "@mui/material/Switch";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import {ErrorMessage, useFormikContext} from "formik"; import {ErrorMessage, useFormikContext} from "formik";
import React, {useEffect, useState} from "react";
import colors from "qqq/assets/theme/base/colors"; import colors from "qqq/assets/theme/base/colors";
import MDTypography from "qqq/components/legacy/MDTypography"; import MDTypography from "qqq/components/legacy/MDTypography";
import Client from "qqq/utils/qqq/Client"; import Client from "qqq/utils/qqq/Client";
import React, {useEffect, useState} from "react";
interface Props interface Props
{ {
tableName?: string; tableName?: string;
processName?: string; processName?: string;
fieldName: string; fieldName?: string;
possibleValueSourceName?: string;
overrideId?: string; overrideId?: string;
fieldLabel: string; fieldLabel: string;
inForm: boolean; inForm: boolean;
@ -57,6 +58,8 @@ interface Props
DynamicSelect.defaultProps = { DynamicSelect.defaultProps = {
tableName: null, tableName: null,
processName: null, processName: null,
fieldName: null,
possibleValueSourceName: null,
inForm: true, inForm: true,
initialValue: null, initialValue: null,
initialDisplayValue: null, initialDisplayValue: null,
@ -73,16 +76,78 @@ DynamicSelect.defaultProps = {
}, },
}; };
const {inputBorderColor} = colors;
export const getAutocompleteOutlinedStyle = (isDisabled: boolean) =>
{
return ({
"& .MuiOutlinedInput-root": {
borderRadius: "0.75rem",
},
"& .MuiInputBase-root": {
padding: "0.5rem",
background: isDisabled ? "#f0f2f5!important" : "initial",
},
"& .MuiOutlinedInput-root .MuiAutocomplete-input": {
padding: "0",
fontSize: "1rem"
},
"& .Mui-disabled .MuiOutlinedInput-notchedOutline": {
borderColor: inputBorderColor
}
});
}
const qController = Client.getInstance(); const qController = Client.getInstance();
function DynamicSelect({tableName, processName, fieldName, overrideId, fieldLabel, inForm, initialValue, initialDisplayValue, initialValues, onChange, isEditable, isMultiple, bulkEditMode, bulkEditSwitchChangeHandler, otherValues, variant, initiallyOpen}: Props) function DynamicSelect({tableName, processName, fieldName, possibleValueSourceName, overrideId, fieldLabel, inForm, initialValue, initialDisplayValue, initialValues, onChange, isEditable, isMultiple, bulkEditMode, bulkEditSwitchChangeHandler, otherValues, variant, initiallyOpen}: Props)
{ {
const [open, setOpen] = useState(initiallyOpen); const [open, setOpen] = useState(initiallyOpen);
const [options, setOptions] = useState<readonly QPossibleValue[]>([]); const [options, setOptions] = useState<readonly QPossibleValue[]>([]);
const [searchTerm, setSearchTerm] = useState(null); const [searchTerm, setSearchTerm] = useState(null);
const [firstRender, setFirstRender] = useState(true); const [firstRender, setFirstRender] = useState(true);
const [otherValuesWhenResultsWereLoaded, setOtherValuesWhenResultsWereLoaded] = useState(JSON.stringify(Object.fromEntries((otherValues)))) const [otherValuesWhenResultsWereLoaded, setOtherValuesWhenResultsWereLoaded] = useState(JSON.stringify(Object.fromEntries((otherValues))))
const {inputBorderColor} = colors;
useEffect(() =>
{
if(tableName && processName)
{
console.log("DynamicSelect - you may not provide both a tableName and a processName")
}
if(tableName && !fieldName)
{
console.log("DynamicSelect - if you provide a tableName, you must also provide a fieldName");
}
if(processName && !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)
{
console.log("DynamicSelect - you must provide either a fieldName (and a tableName or processName) or a possibleValueSourceName");
}
if(fieldName)
{
if(!tableName || !processName)
{
console.log("DynamicSelect - if you provide a fieldName, you must also provide a tableName or processName");
}
}
if(possibleValueSourceName)
{
if(tableName || processName)
{
console.log("DynamicSelect - if you provide a possibleValueSourceName, you should not also provide a tableName or processName");
}
}
}, [tableName, processName, fieldName, possibleValueSourceName]);
//////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////
// default value - needs to be an array (from initialValues (array) prop) for multiple mode - // // default value - needs to be an array (from initialValues (array) prop) for multiple mode - //
@ -133,7 +198,7 @@ function DynamicSelect({tableName, processName, fieldName, overrideId, fieldLabe
(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, searchTerm ?? "", null, otherValues); const results: QPossibleValue[] = await qController.possibleValues(tableName, processName, fieldName ?? possibleValueSourceName, searchTerm ?? "", null, otherValues);
if(tableMetaData == null && tableName) if(tableMetaData == null && tableName)
{ {
@ -166,7 +231,7 @@ function DynamicSelect({tableName, processName, fieldName, overrideId, fieldLabe
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, searchTerm ?? "", null, otherValues); const results: QPossibleValue[] = await qController.possibleValues(tableName, processName, fieldName ?? possibleValueSourceName, searchTerm ?? "", null, otherValues);
setLoading(false); setLoading(false);
setOptions([ ...results ]); setOptions([ ...results ]);
setOtherValuesWhenResultsWereLoaded(JSON.stringify(Object.fromEntries(otherValues))); setOtherValuesWhenResultsWereLoaded(JSON.stringify(Object.fromEntries(otherValues)));
@ -206,7 +271,7 @@ function DynamicSelect({tableName, processName, fieldName, overrideId, fieldLabe
onChange(value ? new QPossibleValue(value) : null); onChange(value ? new QPossibleValue(value) : null);
} }
} }
else if(setFieldValueRef) else if(setFieldValueRef && fieldName)
{ {
setFieldValueRef(fieldName, value ? value.id : null); setFieldValueRef(fieldName, value ? value.id : null);
} }
@ -282,28 +347,13 @@ function DynamicSelect({tableName, processName, fieldName, overrideId, fieldLabe
let autocompleteSX = {}; let autocompleteSX = {};
if (variant == "outlined") if (variant == "outlined")
{ {
autocompleteSX = { autocompleteSX = getAutocompleteOutlinedStyle(isDisabled);
"& .MuiOutlinedInput-root": {
borderRadius: "0.75rem",
},
"& .MuiInputBase-root": {
padding: "0.5rem",
background: isDisabled ? "#f0f2f5!important" : "initial",
},
"& .MuiOutlinedInput-root .MuiAutocomplete-input": {
padding: "0",
fontSize: "1rem"
},
"& .Mui-disabled .MuiOutlinedInput-notchedOutline": {
borderColor: inputBorderColor
}
}
} }
const autocomplete = ( const autocomplete = (
<Box> <Box>
<Autocomplete <Autocomplete
id={overrideId ?? fieldName} id={overrideId ?? fieldName ?? possibleValueSourceName}
sx={autocompleteSX} sx={autocompleteSX}
open={open} open={open}
fullWidth fullWidth
@ -383,7 +433,7 @@ function DynamicSelect({tableName, processName, fieldName, overrideId, fieldLabe
inForm && inForm &&
<Box mt={0.75}> <Box mt={0.75}>
<MDTypography component="div" variant="caption" color="error" fontWeight="regular"> <MDTypography component="div" variant="caption" color="error" fontWeight="regular">
{!isDisabled && <div className="fieldErrorMessage"><ErrorMessage name={fieldName} render={msg => <span data-field-error="true">{msg}</span>} /></div>} {!isDisabled && <div className="fieldErrorMessage"><ErrorMessage name={fieldName ?? possibleValueSourceName} render={msg => <span data-field-error="true">{msg}</span>} /></div>}
</MDTypography> </MDTypography>
</Box> </Box>
} }

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,7 +231,7 @@ 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));
@ -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 ({
slotProps: {
tooltip: { tooltip: {
sx: { sx: {
maxWidth: maxWidth 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>
<MenuItem disabled={disabledBecauseNotOwner} onClick={() => handleDropdownOptionClick(SAVE_OPTION)}>
<ListItemIcon><Icon>save</Icon></ListItemIcon> <ListItemIcon><Icon>save</Icon></ListItemIcon>
{currentSavedView ? "Save..." : "Save As..."} {currentSavedView ? "Save..." : "Save As..."}
</MenuItem> </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>
<MenuItem disabled={currentSavedView === null || disabledBecauseNotOwner} onClick={() => handleDropdownOptionClick(RENAME_OPTION)}>
<ListItemIcon><Icon>edit</Icon></ListItemIcon> <ListItemIcon><Icon>edit</Icon></ListItemIcon>
Rename... Rename...
</MenuItem> </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.">
<span>
<MenuItem disabled={currentSavedView === null} onClick={() => handleDropdownOptionClick(DUPLICATE_OPTION)}> <MenuItem disabled={currentSavedView === null} onClick={() => handleDropdownOptionClick(DUPLICATE_OPTION)}>
<ListItemIcon><Icon>content_copy</Icon></ListItemIcon> <ListItemIcon><Icon>content_copy</Icon></ListItemIcon>
Save As... Save As...
</MenuItem> </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>
<MenuItem disabled={currentSavedView === null || disabledBecauseNotOwner} onClick={() => handleDropdownOptionClick(DELETE_OPTION)}>
<ListItemIcon><Icon>delete</Icon></ListItemIcon> <ListItemIcon><Icon>delete</Icon></ListItemIcon>
Delete... Delete...
</MenuItem> </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.">
<span>
<MenuItem onClick={() => handleDropdownOptionClick(CLEAR_OPTION)}> <MenuItem onClick={() => handleDropdownOptionClick(CLEAR_OPTION)}>
<ListItemIcon><Icon>monitor</Icon></ListItemIcon> <ListItemIcon><Icon>monitor</Icon></ListItemIcon>
New View New View
</MenuItem> </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.">
<span>
<MenuItem onClick={() => handleDropdownOptionClick(NEW_REPORT_OPTION)}> <MenuItem onClick={() => handleDropdownOptionClick(NEW_REPORT_OPTION)}>
<ListItemIcon><Icon>article</Icon></ListItemIcon> <ListItemIcon><Icon>article</Icon></ListItemIcon>
Create Report from Current View Create Report from Current View
</MenuItem> </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

@ -0,0 +1,482 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import {QJobComplete} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobComplete";
import {QJobError} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobError";
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
import {Alert} from "@mui/material";
import Autocomplete from "@mui/material/Autocomplete";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import Card from "@mui/material/Card";
import Icon from "@mui/material/Icon";
import Modal from "@mui/material/Modal";
import TextField from "@mui/material/TextField";
import Tooltip from "@mui/material/Tooltip/Tooltip";
import Typography from "@mui/material/Typography";
import FormData from "form-data";
import colors from "qqq/assets/theme/base/colors";
import {QCancelButton} from "qqq/components/buttons/DefaultButtons";
import DynamicSelect, {getAutocompleteOutlinedStyle} from "qqq/components/forms/DynamicSelect";
import Client from "qqq/utils/qqq/Client";
import React, {useEffect, useReducer, useState} from "react";
interface ShareModalProps
{
open: boolean;
onClose: () => void;
tableMetaData: QTableMetaData;
record: QRecord;
}
ShareModal.defaultProps = {};
interface CurrentShare
{
shareId: any;
scopeId: string;
audienceType: string;
audienceId: any;
audienceLabel: string;
}
interface Scope
{
id: string;
label: string;
}
const scopeOptions: Scope[] = [
{id: "READ_ONLY", label: "Read-Only"},
{id: "READ_WRITE", label: "Read and Edit"}
];
const defaultScope = scopeOptions[0];
const qController = Client.getInstance();
interface ShareableTableMetaData
{
sharedRecordTableName: string;
assetIdFieldName: string;
scopeFieldName: string;
audienceTypesPossibleValueSourceName: string;
audiencePossibleValueSourceName: string;
thisTableOwnerIdFieldName: string;
audienceTypes: {[name: string]: any}; // values here are: ShareableAudienceType
}
/*******************************************************************************
** component containing a Modal dialog for sharing records
*******************************************************************************/
export default function ShareModal({open, onClose, tableMetaData, record}: ShareModalProps): JSX.Element
{
const [statusString, setStatusString] = useState("Loading...");
const [alert, setAlert] = useState(null as string);
const [selectedAudienceOption, setSelectedAudienceOption] = useState(null as {id: string, label: string});
const [selectedAudienceType, setSelectedAudienceType] = useState(null);
const [selectedAudienceId, setSelectedAudienceId] = useState(null);
const [selectedScopeId, setSelectedScopeId] = useState(defaultScope.id);
const [submitting, setSubmitting] = useState(false);
const [currentShares, setCurrentShares] = useState([] as CurrentShare[])
const [needToLoadCurrentShares, setNeedToLoadCurrentShares] = useState(true);
const [everLoadedCurrentShares, setEverLoadedCurrentShares] = useState(false);
const shareableTableMetaData = tableMetaData.shareableTableMetaData as ShareableTableMetaData;
const [, forceUpdate] = useReducer((x) => x + 1, 0);
if(!shareableTableMetaData)
{
console.error(`Did not find a shareableTableMetaData on table ${tableMetaData.name}`);
}
/////////////////////////////////////////////////////////
// trigger initial load, and post any changes, re-load //
/////////////////////////////////////////////////////////
useEffect(() =>
{
if(needToLoadCurrentShares)
{
loadShares();
}
}, [needToLoadCurrentShares]);
/*******************************************************************************
**
*******************************************************************************/
function close(event: object, reason: string)
{
if (reason === "backdropClick" || reason === "escapeKeyDown")
{
return;
}
onClose();
}
/*******************************************************************************
**
*******************************************************************************/
function handleAudienceChange(value: any | any[], reason: string)
{
if(value)
{
const [audienceType, audienceId] = value.id.split(":");
setSelectedAudienceType(audienceType);
setSelectedAudienceId(audienceId);
}
else
{
setSelectedAudienceType(null);
setSelectedAudienceId(null);
}
}
/*******************************************************************************
**
*******************************************************************************/
function handleScopeChange(event: React.SyntheticEvent, value: any | any[], reason: string)
{
if(value)
{
setSelectedScopeId(value.id);
}
else
{
setSelectedScopeId(null);
}
}
/*******************************************************************************
**
*******************************************************************************/
async function editingExistingShareScope(shareId: number, value: any | any[])
{
setStatusString("Saving...");
setAlert(null);
const formData = new FormData();
formData.append("tableName", tableMetaData.name);
formData.append("recordId", record.values.get(tableMetaData.primaryKeyField));
formData.append("shareId", shareId);
formData.append("scopeId", value.id);
const processResult = await qController.processRun("editSharedRecord", formData, null, true);
if (processResult instanceof QJobError)
{
const jobError = processResult as QJobError;
setStatusString(null);
setAlert("Error editing shared record: " + jobError.error);
setSubmitting(false)
}
else
{
const result = processResult as QJobComplete;
setStatusString(null);
setAlert(null);
setNeedToLoadCurrentShares(true);
setSubmitting(false)
}
}
/*******************************************************************************
**
*******************************************************************************/
async function loadShares()
{
setNeedToLoadCurrentShares(false);
const formData = new FormData();
formData.append("tableName", tableMetaData.name);
formData.append("recordId", record.values.get(tableMetaData.primaryKeyField));
const processResult = await qController.processRun("getSharedRecords", formData, null, true);
setStatusString("Loading...");
setAlert(null)
if (processResult instanceof QJobError)
{
const jobError = processResult as QJobError;
setStatusString(null);
setAlert("Error loading: " + jobError.error);
}
else
{
const result = processResult as QJobComplete;
const newCurrentShares: CurrentShare[] = [];
for (let i in result.values["resultList"])
{
newCurrentShares.push(result.values["resultList"][i].values);
}
setCurrentShares(newCurrentShares);
setEverLoadedCurrentShares(true);
setStatusString(null);
setAlert(null);
}
}
/*******************************************************************************
**
*******************************************************************************/
async function saveNewShare()
{
setSubmitting(true)
setStatusString("Saving...");
setAlert(null);
const formData = new FormData();
formData.append("tableName", tableMetaData.name);
formData.append("recordId", record.values.get(tableMetaData.primaryKeyField));
formData.append("audienceType", selectedAudienceType);
formData.append("audienceId", selectedAudienceId);
formData.append("scopeId", selectedScopeId);
const processResult = await qController.processRun("insertSharedRecord", formData, null, true);
if (processResult instanceof QJobError)
{
const jobError = processResult as QJobError;
setStatusString(null);
setAlert("Error sharing record: " + jobError.error);
setSubmitting(false)
}
else
{
const result = processResult as QJobComplete;
setStatusString(null);
setAlert(null);
setSelectedAudienceOption(null);
setNeedToLoadCurrentShares(true);
setSubmitting(false)
}
}
/*******************************************************************************
**
*******************************************************************************/
async function removeShare(shareId: number)
{
setStatusString("Deleting...");
setAlert(null);
const formData = new FormData();
formData.append("tableName", tableMetaData.name);
formData.append("recordId", record.values.get(tableMetaData.primaryKeyField));
formData.append("shareId", shareId);
const processResult = await qController.processRun("deleteSharedRecord", formData, null, true);
if (processResult instanceof QJobError)
{
const jobError = processResult as QJobError;
setStatusString(null);
setAlert("Error deleting share: " + jobError.error);
}
else
{
const result = processResult as QJobComplete;
setNeedToLoadCurrentShares(true);
setStatusString(null);
setAlert(null);
}
}
/*******************************************************************************
**
*******************************************************************************/
function getScopeOption(scopeId: string): Scope
{
for (let scopeOption of scopeOptions)
{
if(scopeOption.id == scopeId)
{
return (scopeOption);
}
}
return (null);
}
/*******************************************************************************
**
*******************************************************************************/
function renderScopeDropdown(id: string, defaultValue: Scope, onChange: (event: React.SyntheticEvent, value: any | any[], reason: string) => void)
{
const isDisabled = (id == "new-share-scope" && submitting);
return (
<Autocomplete
id={id}
disabled={isDisabled}
renderInput={(params) => (<TextField {...params} label="Scope" variant="outlined" autoComplete="off" type="search" InputProps={{...params.InputProps}} />)}
options={scopeOptions}
// @ts-ignore
defaultValue={defaultValue}
onChange={onChange}
isOptionEqualToValue={(option, value) => option.id === value.id}
// @ts-ignore Property label does not exist on string | {thing with label}
getOptionLabel={(option) => option.label}
autoSelect={true}
autoHighlight={true}
disableClearable
fullWidth
sx={getAutocompleteOutlinedStyle(isDisabled)}
/>
);
}
//////////////////////
// render the modal //
//////////////////////
return (<Modal open={open} onClose={close}>
<div className="share">
<Box sx={{position: "absolute", overflowY: "auto", maxHeight: "100%", width: "100%", display: "flex", height: "100%", flexDirection: "column", justifyContent: "center"}}>
<Card sx={{my: 5, mx: "auto", p: 3}}>
{/* header */}
<Box display="flex" flexDirection="row" justifyContent="space-between" alignItems="flex-start">
<Typography variant="h4" pb={1} fontWeight="600">
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">
{/* todo move to helpContent (what do we attach the meta-data too??) */}
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.
</Box>
<Box fontSize={14} maxWidth="590px" pb={1} fontWeight="300">
{alert && <Alert color="error" onClose={() => setAlert(null)}>{alert}</Alert>}
{statusString}
{!alert && !statusString && (<>&nbsp;</>)}
</Box>
</Typography>
</Box>
{/* body */}
<Box pb={3} display="flex" flexDirection="column">
{/* row for adding a new share */}
<Box display="flex" flexDirection="row" alignItems="center">
<Box width="350px" pr={2} mb={-1.5}>
<DynamicSelect
possibleValueSourceName={shareableTableMetaData.audiencePossibleValueSourceName}
fieldLabel="User or Group" // todo should come from shareableTableMetaData
initialValue={selectedAudienceOption?.id}
initialDisplayValue={selectedAudienceOption?.label}
inForm={false}
onChange={handleAudienceChange}
/>
</Box>
<Box width="180px" pr={2}>
{renderScopeDropdown("new-share-scope", defaultScope, handleScopeChange)}
</Box>
<Box>
<Tooltip title={selectedAudienceId == null ? "Select a user or group to share with." : null}>
<span>
<Button disabled={submitting || selectedAudienceId == null} sx={iconButtonSX} onClick={() => saveNewShare()}>
<Icon color={selectedAudienceId == null ? "secondary" : "info"}>save</Icon>
</Button>
</span>
</Tooltip>
</Box>
</Box>
{/* row showing existing shares */}
<Box pt={3}>
<Box pb="0.25rem">
<h5 style={{fontWeight: "600"}}>Current Shares
{
everLoadedCurrentShares ? <>&nbsp;({currentShares.length})</> : <></>
}
</h5>
</Box>
<Box sx={{border: `1px solid ${colors.grayLines.main}`, borderRadius: "1rem", overflow: "hidden"}}>
<Box sx={{overflow: "auto"}} height="210px" pt="0.75rem">
{
currentShares.map((share) => (
<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 width="310px" 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>
</Box>
<Box pr="1rem">
<Button sx={{...iconButtonSX, ...redIconButton}} onClick={() => removeShare(share.shareId)}><Icon>clear</Icon></Button>
</Box>
</Box>
))
}
</Box>
</Box>
</Box>
</Box>
{/* footer */}
<Box display="flex" flexDirection="row" justifyContent="flex-end">
<QCancelButton label="Done" iconName="check" onClickHandler={() => close(null, null)} disabled={false} />
</Box>
</Card>
</Box>
</div>
</Modal>);
}
const iconButtonSX =
{
border: `1px solid ${colors.grayLines.main} !important`,
borderRadius: "0.75rem",
textTransform: "none",
fontSize: "1rem",
fontWeight: "400",
width: "40px",
minWidth: "40px",
paddingLeft: 0,
paddingRight: 0,
color: colors.secondary.main,
"&:hover": {color: colors.secondary.main},
"&:focus": {color: colors.secondary.main},
"&:focus:not(:hover)": {color: colors.secondary.main},
};
const redIconButton =
{
color: colors.error.main,
"&:hover": {color: colors.error.main},
"&:focus": {color: colors.error.main},
"&:focus:not(:hover)": {color: colors.error.main},
};

View File

@ -49,11 +49,13 @@ import Tooltip from "@mui/material/Tooltip/Tooltip";
import QContext from "QContext"; import QContext from "QContext";
import colors from "qqq/assets/theme/base/colors"; import colors from "qqq/assets/theme/base/colors";
import AuditBody from "qqq/components/audits/AuditBody"; import AuditBody from "qqq/components/audits/AuditBody";
import {QActionsMenuButton, QCancelButton, QDeleteButton, QEditButton} from "qqq/components/buttons/DefaultButtons"; import {QActionsMenuButton, QCancelButton, QDeleteButton, QEditButton, standardWidth} from "qqq/components/buttons/DefaultButtons";
import EntityForm from "qqq/components/forms/EntityForm"; import EntityForm from "qqq/components/forms/EntityForm";
import MDButton from "qqq/components/legacy/MDButton";
import {GotoRecordButton} from "qqq/components/misc/GotoRecordDialog"; import {GotoRecordButton} from "qqq/components/misc/GotoRecordDialog";
import HelpContent, {hasHelpContent} from "qqq/components/misc/HelpContent"; import HelpContent, {hasHelpContent} from "qqq/components/misc/HelpContent";
import QRecordSidebar from "qqq/components/misc/RecordSidebar"; import QRecordSidebar from "qqq/components/misc/RecordSidebar";
import ShareModal from "qqq/components/sharing/ShareModal";
import DashboardWidgets from "qqq/components/widgets/DashboardWidgets"; import DashboardWidgets from "qqq/components/widgets/DashboardWidgets";
import BaseLayout from "qqq/layouts/BaseLayout"; import BaseLayout from "qqq/layouts/BaseLayout";
import ProcessRun from "qqq/pages/processes/ProcessRun"; import ProcessRun from "qqq/pages/processes/ProcessRun";
@ -82,6 +84,10 @@ RecordView.defaultProps =
const TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT = "qqq.tableVariant"; const TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT = "qqq.tableVariant";
/*******************************************************************************
** Record View Screen component.
*******************************************************************************/
function RecordView({table, launchProcess}: Props): JSX.Element function RecordView({table, launchProcess}: Props): JSX.Element
{ {
const {id} = useParams(); const {id} = useParams();
@ -117,11 +123,14 @@ function RecordView({table, launchProcess}: Props): JSX.Element
const [launchingProcess, setLaunchingProcess] = useState(launchProcess); const [launchingProcess, setLaunchingProcess] = useState(launchProcess);
const [showEditChildForm, setShowEditChildForm] = useState(null as any); const [showEditChildForm, setShowEditChildForm] = useState(null as any);
const [showAudit, setShowAudit] = useState(false); const [showAudit, setShowAudit] = useState(false);
const [showShareModal, setShowShareModal] = useState(false);
const [isDeleteSubmitting, setIsDeleteSubmitting] = useState(false);
const openActionsMenu = (event: any) => setActionsMenu(event.currentTarget); const openActionsMenu = (event: any) => setActionsMenu(event.currentTarget);
const closeActionsMenu = () => setActionsMenu(null); const closeActionsMenu = () => setActionsMenu(null);
const {accentColor, setPageHeader, tableMetaData, setTableMetaData, tableProcesses, setTableProcesses, dotMenuOpen, keyboardHelpOpen, helpHelpActive, recordAnalytics} = useContext(QContext); const {accentColor, setPageHeader, tableMetaData, setTableMetaData, tableProcesses, setTableProcesses, dotMenuOpen, keyboardHelpOpen, helpHelpActive, recordAnalytics, userId: currentUserId} = useContext(QContext);
if (localStorage.getItem(tableVariantLocalStorageKey)) if (localStorage.getItem(tableVariantLocalStorageKey))
{ {
@ -622,6 +631,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element
const handleClickDeleteButton = () => const handleClickDeleteButton = () =>
{ {
setDeleteConfirmationOpen(true); setDeleteConfirmationOpen(true);
setIsDeleteSubmitting(false);
}; };
const handleDeleteConfirmClose = () => const handleDeleteConfirmClose = () =>
@ -631,6 +641,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element
const handleDelete = (event: { preventDefault: () => void }) => const handleDelete = (event: { preventDefault: () => void }) =>
{ {
setIsDeleteSubmitting(true);
event?.preventDefault(); event?.preventDefault();
(async () => (async () =>
{ {
@ -639,11 +650,13 @@ function RecordView({table, launchProcess}: Props): JSX.Element
await qController.delete(tableName, id) await qController.delete(tableName, id)
.then(() => .then(() =>
{ {
setIsDeleteSubmitting(false);
const path = pathParts.slice(0, -1).join("/"); const path = pathParts.slice(0, -1).join("/");
navigate(path, {state: {deleteSuccess: true}}); navigate(path, {state: {deleteSuccess: true}});
}) })
.catch((error) => .catch((error) =>
{ {
setIsDeleteSubmitting(false);
setDeleteConfirmationOpen(false); setDeleteConfirmationOpen(false);
console.log("Caught:"); console.log("Caught:");
console.log(error); console.log(error);
@ -759,6 +772,68 @@ function RecordView({table, launchProcess}: Props): JSX.Element
</Menu> </Menu>
); );
/*******************************************************************************
** function to open the sharing modal
*******************************************************************************/
const openShareModal = () =>
{
setShowShareModal(true);
};
/*******************************************************************************
** function to close the sharing modal
*******************************************************************************/
const closeShareModal = () =>
{
setShowShareModal(false);
};
/*******************************************************************************
** render the share button (if allowed for table)
*******************************************************************************/
const renderShareButton = () =>
{
if (tableMetaData && tableMetaData.shareableTableMetaData)
{
let shareDisabled = true;
let disabledTooltipText = "";
if(tableMetaData.shareableTableMetaData.thisTableOwnerIdFieldName && record)
{
const ownerId = record.values.get(tableMetaData.shareableTableMetaData.thisTableOwnerIdFieldName);
if(ownerId != currentUserId)
{
disabledTooltipText = `Only the owner of a ${tableMetaData.label} may share it.`
shareDisabled = true;
}
else
{
disabledTooltipText = "";
shareDisabled = false;
}
}
else
{
shareDisabled = false;
}
return (<Box width={standardWidth} mr={2}>
<Tooltip title={disabledTooltipText}>
<span>
<MDButton id="shareButton" type="button" color="info" size="small" onClick={() => openShareModal()} fullWidth startIcon={<Icon>group_add</Icon>} disabled={shareDisabled}>
Share
</MDButton>
</span>
</Tooltip>
</Box>);
}
return (<React.Fragment />);
};
const openModalProcess = (process: QProcessMetaData = null) => const openModalProcess = (process: QProcessMetaData = null) =>
{ {
navigate(process.name); navigate(process.name);
@ -914,6 +989,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element
</Typography> </Typography>
<Box display="flex"> <Box display="flex">
<GotoRecordButton metaData={metaData} tableMetaData={tableMetaData} /> <GotoRecordButton metaData={metaData} tableMetaData={tableMetaData} />
{renderShareButton()}
<QActionsMenuButton isOpen={actionsMenu} onClickHandler={openActionsMenu} /> <QActionsMenuButton isOpen={actionsMenu} onClickHandler={openActionsMenu} />
</Box> </Box>
{renderActionsMenu} {renderActionsMenu}
@ -962,7 +1038,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={handleDeleteConfirmClose}>No</Button> <Button onClick={handleDeleteConfirmClose}>No</Button>
<Button onClick={handleDelete} autoFocus> <Button onClick={handleDelete} autoFocus disabled={isDeleteSubmitting}>
Yes Yes
</Button> </Button>
</DialogActions> </DialogActions>
@ -1010,6 +1086,11 @@ function RecordView({table, launchProcess}: Props): JSX.Element
</Modal> </Modal>
} }
{
showShareModal && tableMetaData && record &&
<ShareModal open={showShareModal} onClose={closeShareModal} tableMetaData={tableMetaData} record={record}></ShareModal>
}
</Box> </Box>
} }
</Box> </Box>

View File

@ -47,6 +47,8 @@ module.exports = function (app)
app.use("/download/*", getRequestHandler()); app.use("/download/*", getRequestHandler());
app.use("/metaData/*", getRequestHandler()); app.use("/metaData/*", getRequestHandler());
app.use("/data/*", getRequestHandler()); app.use("/data/*", getRequestHandler());
app.use("/possibleValues/*", getRequestHandler());
app.use("/possibleValues", getRequestHandler());
app.use("/widget/*", getRequestHandler()); app.use("/widget/*", getRequestHandler());
app.use("/serverInfo", getRequestHandler()); app.use("/serverInfo", getRequestHandler());
app.use("/manageSession", getRequestHandler()); app.use("/manageSession", getRequestHandler());