mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-21 14:48:43 +00:00
Compare commits
1 Commits
snapshot-f
...
snapshot-f
Author | SHA1 | Date | |
---|---|---|---|
11f1250d73 |
18466
package-lock.json
generated
18466
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -6,7 +6,7 @@
|
||||
"@auth0/auth0-react": "1.10.2",
|
||||
"@emotion/react": "11.7.1",
|
||||
"@emotion/styled": "11.6.0",
|
||||
"@kingsrook/qqq-frontend-core": "1.0.97",
|
||||
"@kingsrook/qqq-frontend-core": "1.0.96",
|
||||
"@mui/icons-material": "5.4.1",
|
||||
"@mui/material": "5.11.1",
|
||||
"@mui/styles": "5.11.1",
|
||||
|
@ -664,16 +664,10 @@ export default function App()
|
||||
const [dotMenuOpen, setDotMenuOpen] = useState(false);
|
||||
const [keyboardHelpOpen, setKeyboardHelpOpen] = useState(false);
|
||||
const [helpHelpActive] = useState(queryParams.has("helpHelp"));
|
||||
const [userId, setUserId] = useState(user?.email);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
setUserId(user?.email)
|
||||
}, [user]);
|
||||
|
||||
|
||||
const [googleAnalyticsUtils] = useState(new GoogleAnalyticsUtils());
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -695,7 +689,6 @@ export default function App()
|
||||
dotMenuOpen: dotMenuOpen,
|
||||
keyboardHelpOpen: keyboardHelpOpen,
|
||||
helpHelpActive: helpHelpActive,
|
||||
userId: userId,
|
||||
setPageHeader: (header: string | JSX.Element) => setPageHeader(header),
|
||||
setAccentColor: (accentColor: string) => setAccentColor(accentColor),
|
||||
setAccentColorLight: (accentColorLight: string) => setAccentColorLight(accentColorLight),
|
||||
|
@ -59,7 +59,6 @@ interface QContext
|
||||
pathToLabelMap?: {[path: string]: string};
|
||||
branding?: QBrandingMetaData;
|
||||
helpHelpActive?: boolean;
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
const defaultState = {
|
||||
|
@ -28,17 +28,16 @@ import Box from "@mui/material/Box";
|
||||
import Switch from "@mui/material/Switch";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import {ErrorMessage, useFormikContext} from "formik";
|
||||
import React, {useEffect, useState} from "react";
|
||||
import colors from "qqq/assets/theme/base/colors";
|
||||
import MDTypography from "qqq/components/legacy/MDTypography";
|
||||
import Client from "qqq/utils/qqq/Client";
|
||||
import React, {useEffect, useState} from "react";
|
||||
|
||||
interface Props
|
||||
{
|
||||
tableName?: string;
|
||||
processName?: string;
|
||||
fieldName?: string;
|
||||
possibleValueSourceName?: string;
|
||||
fieldName: string;
|
||||
overrideId?: string;
|
||||
fieldLabel: string;
|
||||
inForm: boolean;
|
||||
@ -58,8 +57,6 @@ interface Props
|
||||
DynamicSelect.defaultProps = {
|
||||
tableName: null,
|
||||
processName: null,
|
||||
fieldName: null,
|
||||
possibleValueSourceName: null,
|
||||
inForm: true,
|
||||
initialValue: null,
|
||||
initialDisplayValue: null,
|
||||
@ -76,78 +73,16 @@ 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();
|
||||
|
||||
function DynamicSelect({tableName, processName, fieldName, possibleValueSourceName, overrideId, fieldLabel, inForm, initialValue, initialDisplayValue, initialValues, onChange, isEditable, isMultiple, bulkEditMode, bulkEditSwitchChangeHandler, otherValues, variant, initiallyOpen}: Props)
|
||||
function DynamicSelect({tableName, processName, fieldName, overrideId, fieldLabel, inForm, initialValue, initialDisplayValue, initialValues, onChange, isEditable, isMultiple, bulkEditMode, bulkEditSwitchChangeHandler, otherValues, variant, initiallyOpen}: Props)
|
||||
{
|
||||
const [open, setOpen] = useState(initiallyOpen);
|
||||
const [options, setOptions] = useState<readonly QPossibleValue[]>([]);
|
||||
const [searchTerm, setSearchTerm] = useState(null);
|
||||
const [firstRender, setFirstRender] = useState(true);
|
||||
const [otherValuesWhenResultsWereLoaded, setOtherValuesWhenResultsWereLoaded] = useState(JSON.stringify(Object.fromEntries((otherValues))))
|
||||
|
||||
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]);
|
||||
const {inputBorderColor} = colors;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// default value - needs to be an array (from initialValues (array) prop) for multiple mode - //
|
||||
@ -198,7 +133,7 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
||||
(async () =>
|
||||
{
|
||||
// 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, fieldName, searchTerm ?? "", null, otherValues);
|
||||
|
||||
if(tableMetaData == null && tableName)
|
||||
{
|
||||
@ -231,7 +166,7 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
||||
setLoading(true);
|
||||
setOptions([]);
|
||||
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, fieldName, searchTerm ?? "", null, otherValues);
|
||||
setLoading(false);
|
||||
setOptions([ ...results ]);
|
||||
setOtherValuesWhenResultsWereLoaded(JSON.stringify(Object.fromEntries(otherValues)));
|
||||
@ -271,7 +206,7 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
||||
onChange(value ? new QPossibleValue(value) : null);
|
||||
}
|
||||
}
|
||||
else if(setFieldValueRef && fieldName)
|
||||
else if(setFieldValueRef)
|
||||
{
|
||||
setFieldValueRef(fieldName, value ? value.id : null);
|
||||
}
|
||||
@ -347,13 +282,28 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
||||
let autocompleteSX = {};
|
||||
if (variant == "outlined")
|
||||
{
|
||||
autocompleteSX = getAutocompleteOutlinedStyle(isDisabled);
|
||||
autocompleteSX = {
|
||||
"& .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 = (
|
||||
<Box>
|
||||
<Autocomplete
|
||||
id={overrideId ?? fieldName ?? possibleValueSourceName}
|
||||
id={overrideId ?? fieldName}
|
||||
sx={autocompleteSX}
|
||||
open={open}
|
||||
fullWidth
|
||||
@ -433,7 +383,7 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
||||
inForm &&
|
||||
<Box mt={0.75}>
|
||||
<MDTypography component="div" variant="caption" color="error" fontWeight="regular">
|
||||
{!isDisabled && <div className="fieldErrorMessage"><ErrorMessage name={fieldName ?? possibleValueSourceName} render={msg => <span data-field-error="true">{msg}</span>} /></div>}
|
||||
{!isDisabled && <div className="fieldErrorMessage"><ErrorMessage name={fieldName} render={msg => <span data-field-error="true">{msg}</span>} /></div>}
|
||||
</MDTypography>
|
||||
</Box>
|
||||
}
|
||||
|
@ -25,7 +25,8 @@ import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QT
|
||||
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, Box, Button} from "@mui/material";
|
||||
import {Alert, Button} from "@mui/material";
|
||||
import Box from "@mui/material/Box";
|
||||
import Dialog from "@mui/material/Dialog";
|
||||
import DialogActions from "@mui/material/DialogActions";
|
||||
import DialogContent from "@mui/material/DialogContent";
|
||||
@ -59,7 +60,7 @@ interface Props
|
||||
view?: RecordQueryView;
|
||||
viewAsJson?: string;
|
||||
viewOnChangeCallback?: (selectedSavedViewId: number) => void;
|
||||
loadingSavedView: boolean;
|
||||
loadingSavedView: boolean
|
||||
queryScreenUsage: QueryScreenUsage;
|
||||
}
|
||||
|
||||
@ -68,8 +69,6 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [savedViews, setSavedViews] = useState([] as QRecord[]);
|
||||
const [yourSavedViews, setYourSavedViews] = useState([] as QRecord[]);
|
||||
const [viewsSharedWithYou, setViewsSharedWithYou] = useState([] as QRecord[]);
|
||||
const [savedViewsMenu, setSavedViewsMenu] = useState(null);
|
||||
const [savedViewsHaveLoaded, setSavedViewsHaveLoaded] = useState(false);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
@ -92,7 +91,7 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
|
||||
const CLEAR_OPTION = "New View";
|
||||
const NEW_REPORT_OPTION = "Create Report from Current View";
|
||||
|
||||
const {accentColor, accentColorLight, userId: currentUserId} = useContext(QContext);
|
||||
const {accentColor, accentColorLight} = useContext(QContext);
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
// this component is used by <RecordQuery> - but that component has different usages - //
|
||||
@ -115,13 +114,13 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
|
||||
{
|
||||
setSavedViewsHaveLoaded(true);
|
||||
});
|
||||
}, [location, tableMetaData]);
|
||||
}, [location, tableMetaData])
|
||||
|
||||
|
||||
const baseView = currentSavedView ? JSON.parse(currentSavedView.values.get("viewJson")) as RecordQueryView : tableDefaultView;
|
||||
const viewDiffs = SavedViewUtils.diffViews(tableMetaData, baseView, view);
|
||||
let viewIsModified = false;
|
||||
if (viewDiffs.length > 0)
|
||||
if(viewDiffs.length > 0)
|
||||
{
|
||||
viewIsModified = true;
|
||||
}
|
||||
@ -131,7 +130,7 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
|
||||
*******************************************************************************/
|
||||
async function loadSavedViews()
|
||||
{
|
||||
if (!tableMetaData)
|
||||
if (! tableMetaData)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@ -141,26 +140,10 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
|
||||
|
||||
let savedViews = await makeSavedViewRequest("querySavedView", formData);
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** fired when a saved record is clicked from the dropdown
|
||||
*******************************************************************************/
|
||||
@ -169,13 +152,14 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
|
||||
setSaveFilterPopupOpen(false);
|
||||
closeSavedViewsMenu();
|
||||
viewOnChangeCallback(record.values.get("id"));
|
||||
if (isQueryScreen)
|
||||
if(isQueryScreen)
|
||||
{
|
||||
navigate(`${metaData.getTablePathByName(tableMetaData.name)}/savedView/${record.values.get("id")}`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** fired when a save option is selected from the save... button/dropdown combo
|
||||
*******************************************************************************/
|
||||
@ -187,12 +171,12 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
|
||||
setSaveFilterPopupOpen(true);
|
||||
setIsSaveFilterAs(false);
|
||||
setIsRenameFilter(false);
|
||||
setIsDeleteFilter(false);
|
||||
setIsDeleteFilter(false)
|
||||
|
||||
switch (optionName)
|
||||
switch(optionName)
|
||||
{
|
||||
case SAVE_OPTION:
|
||||
if (currentSavedView == null)
|
||||
if(currentSavedView == null)
|
||||
{
|
||||
setSavedViewNameInputValue("");
|
||||
}
|
||||
@ -202,28 +186,28 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
|
||||
setIsSaveFilterAs(true);
|
||||
break;
|
||||
case CLEAR_OPTION:
|
||||
setSaveFilterPopupOpen(false);
|
||||
setSaveFilterPopupOpen(false)
|
||||
viewOnChangeCallback(null);
|
||||
if (isQueryScreen)
|
||||
if(isQueryScreen)
|
||||
{
|
||||
navigate(metaData.getTablePathByName(tableMetaData.name));
|
||||
}
|
||||
break;
|
||||
case RENAME_OPTION:
|
||||
if (currentSavedView != null)
|
||||
if(currentSavedView != null)
|
||||
{
|
||||
setSavedViewNameInputValue(currentSavedView.values.get("label"));
|
||||
}
|
||||
setIsRenameFilter(true);
|
||||
break;
|
||||
case DELETE_OPTION:
|
||||
setIsDeleteFilter(true);
|
||||
setIsDeleteFilter(true)
|
||||
break;
|
||||
case NEW_REPORT_OPTION:
|
||||
createNewReport();
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -231,11 +215,11 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
|
||||
*******************************************************************************/
|
||||
function createNewReport()
|
||||
{
|
||||
const defaultValues: { [key: string]: any } = {};
|
||||
const defaultValues: {[key: string]: any} = {};
|
||||
defaultValues.tableName = tableMetaData.name;
|
||||
|
||||
let filterForBackend = JSON.parse(JSON.stringify(view.queryFilter));
|
||||
filterForBackend = FilterUtils.prepQueryFilterForBackend(tableMetaData, filterForBackend);
|
||||
filterForBackend = FilterUtils.prepQueryFilterForBackend(tableMetaData, filterForBackend);
|
||||
|
||||
defaultValues.queryFilterJson = JSON.stringify(filterForBackend);
|
||||
defaultValues.columnsJson = JSON.stringify(view.queryColumns);
|
||||
@ -243,6 +227,7 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** fired when save or delete button saved on confirmation dialogs
|
||||
*******************************************************************************/
|
||||
@ -262,7 +247,7 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
|
||||
setSaveFilterPopupOpen(false);
|
||||
setSaveOptionsOpen(false);
|
||||
|
||||
await (async () =>
|
||||
await(async() =>
|
||||
{
|
||||
handleDropdownOptionClick(CLEAR_OPTION);
|
||||
})();
|
||||
@ -282,14 +267,14 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// 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));
|
||||
|
||||
if (isSaveFilterAs || isRenameFilter || currentSavedView == null)
|
||||
{
|
||||
formData.append("label", savedViewNameInputValue);
|
||||
if (currentSavedView != null && isRenameFilter)
|
||||
if(currentSavedView != null && isRenameFilter)
|
||||
{
|
||||
formData.append("id", currentSavedView.values.get("id"));
|
||||
}
|
||||
@ -300,7 +285,7 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
|
||||
formData.append("label", currentSavedView?.values.get("label"));
|
||||
}
|
||||
const recordList = await makeSavedViewRequest("storeSavedView", formData);
|
||||
await (async () =>
|
||||
await(async() =>
|
||||
{
|
||||
if (recordList && recordList.length > 0)
|
||||
{
|
||||
@ -317,11 +302,11 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
|
||||
catch (e: any)
|
||||
{
|
||||
let message = JSON.stringify(e);
|
||||
if (typeof e == "string")
|
||||
if(typeof e == "string")
|
||||
{
|
||||
message = e;
|
||||
}
|
||||
else if (typeof e == "object" && e.message)
|
||||
else if(typeof e == "object" && e.message)
|
||||
{
|
||||
message = e.message;
|
||||
}
|
||||
@ -336,6 +321,7 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** hides/shows the save options
|
||||
*******************************************************************************/
|
||||
@ -345,6 +331,7 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
|
||||
};
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** closes save options menu (on clickaway)
|
||||
*******************************************************************************/
|
||||
@ -359,6 +346,7 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
|
||||
};
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** stores the current dialog input text to state
|
||||
*******************************************************************************/
|
||||
@ -368,6 +356,7 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
|
||||
};
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** closes current dialog
|
||||
*******************************************************************************/
|
||||
@ -377,6 +366,7 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
|
||||
};
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** make a request to the backend for various savedView processes
|
||||
*******************************************************************************/
|
||||
@ -385,7 +375,7 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
|
||||
/////////////////////////
|
||||
// fetch saved filters //
|
||||
/////////////////////////
|
||||
let savedViews = [] as QRecord[];
|
||||
let savedViews = [] as QRecord[]
|
||||
try
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////
|
||||
@ -396,12 +386,12 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
|
||||
if (processResult instanceof QJobError)
|
||||
{
|
||||
const jobError = processResult as QJobError;
|
||||
throw (jobError.error);
|
||||
throw(jobError.error);
|
||||
}
|
||||
else
|
||||
{
|
||||
const result = processResult as QJobComplete;
|
||||
if (result.values.savedViewList)
|
||||
if(result.values.savedViewList)
|
||||
{
|
||||
for (let i = 0; i < result.values.savedViewList.length; i++)
|
||||
{
|
||||
@ -413,7 +403,7 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
throw (e);
|
||||
throw(e);
|
||||
}
|
||||
|
||||
return (savedViews);
|
||||
@ -426,27 +416,17 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
|
||||
|
||||
const tooltipMaxWidth = (maxWidth: string) =>
|
||||
{
|
||||
return ({
|
||||
slotProps: {
|
||||
tooltip: {
|
||||
sx: {
|
||||
maxWidth: maxWidth
|
||||
}
|
||||
return ({slotProps: {
|
||||
tooltip: {
|
||||
sx: {
|
||||
maxWidth: maxWidth
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
}})
|
||||
}
|
||||
|
||||
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 && (
|
||||
<Menu
|
||||
anchorEl={savedViewsMenu}
|
||||
@ -463,101 +443,75 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
|
||||
}
|
||||
{
|
||||
isQueryScreen && hasStorePermission &&
|
||||
<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.</>}>
|
||||
<span>
|
||||
<MenuItem disabled={disabledBecauseNotOwner} onClick={() => handleDropdownOptionClick(SAVE_OPTION)}>
|
||||
<ListItemIcon><Icon>save</Icon></ListItemIcon>
|
||||
{currentSavedView ? "Save..." : "Save As..."}
|
||||
</MenuItem>
|
||||
</span>
|
||||
<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.</>}>
|
||||
<MenuItem onClick={() => handleDropdownOptionClick(SAVE_OPTION)}>
|
||||
<ListItemIcon><Icon>save</Icon></ListItemIcon>
|
||||
{currentSavedView ? "Save..." : "Save As..."}
|
||||
</MenuItem>
|
||||
</Tooltip>
|
||||
}
|
||||
{
|
||||
isQueryScreen && hasStorePermission && currentSavedView != null &&
|
||||
<Tooltip {...menuTooltipAttribs} title={notOwnerTooltipText ?? "Change the name for this saved view."}>
|
||||
<span>
|
||||
<MenuItem disabled={currentSavedView === null || disabledBecauseNotOwner} onClick={() => handleDropdownOptionClick(RENAME_OPTION)}>
|
||||
<ListItemIcon><Icon>edit</Icon></ListItemIcon>
|
||||
Rename...
|
||||
</MenuItem>
|
||||
</span>
|
||||
<Tooltip {...menuTooltipAttribs} title="Change the name for this saved view.">
|
||||
<MenuItem disabled={currentSavedView === null} onClick={() => handleDropdownOptionClick(RENAME_OPTION)}>
|
||||
<ListItemIcon><Icon>edit</Icon></ListItemIcon>
|
||||
Rename...
|
||||
</MenuItem>
|
||||
</Tooltip>
|
||||
}
|
||||
{
|
||||
isQueryScreen && hasStorePermission && currentSavedView != null &&
|
||||
<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)}>
|
||||
<ListItemIcon><Icon>content_copy</Icon></ListItemIcon>
|
||||
Save As...
|
||||
</MenuItem>
|
||||
</span>
|
||||
<MenuItem disabled={currentSavedView === null} onClick={() => handleDropdownOptionClick(DUPLICATE_OPTION)}>
|
||||
<ListItemIcon><Icon>content_copy</Icon></ListItemIcon>
|
||||
Save As...
|
||||
</MenuItem>
|
||||
</Tooltip>
|
||||
}
|
||||
{
|
||||
isQueryScreen && hasDeletePermission && currentSavedView != null &&
|
||||
<Tooltip {...menuTooltipAttribs} title={notOwnerTooltipText ?? "Delete this saved view."}>
|
||||
<span>
|
||||
<MenuItem disabled={currentSavedView === null || disabledBecauseNotOwner} onClick={() => handleDropdownOptionClick(DELETE_OPTION)}>
|
||||
<ListItemIcon><Icon>delete</Icon></ListItemIcon>
|
||||
Delete...
|
||||
</MenuItem>
|
||||
</span>
|
||||
<Tooltip {...menuTooltipAttribs} title="Delete this saved view.">
|
||||
<MenuItem disabled={currentSavedView === null} onClick={() => handleDropdownOptionClick(DELETE_OPTION)}>
|
||||
<ListItemIcon><Icon>delete</Icon></ListItemIcon>
|
||||
Delete...
|
||||
</MenuItem>
|
||||
</Tooltip>
|
||||
}
|
||||
{
|
||||
isQueryScreen &&
|
||||
<Tooltip {...menuTooltipAttribs} title="Create a new view of this table, resetting the filters and columns to their defaults.">
|
||||
<span>
|
||||
<MenuItem onClick={() => handleDropdownOptionClick(CLEAR_OPTION)}>
|
||||
<ListItemIcon><Icon>monitor</Icon></ListItemIcon>
|
||||
New View
|
||||
</MenuItem>
|
||||
</span>
|
||||
<MenuItem onClick={() => handleDropdownOptionClick(CLEAR_OPTION)}>
|
||||
<ListItemIcon><Icon>monitor</Icon></ListItemIcon>
|
||||
New View
|
||||
</MenuItem>
|
||||
</Tooltip>
|
||||
}
|
||||
{
|
||||
isQueryScreen && hasSavedReportsPermission &&
|
||||
<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)}>
|
||||
<ListItemIcon><Icon>article</Icon></ListItemIcon>
|
||||
Create Report from Current View
|
||||
</MenuItem>
|
||||
</span>
|
||||
<MenuItem onClick={() => handleDropdownOptionClick(NEW_REPORT_OPTION)}>
|
||||
<ListItemIcon><Icon>article</Icon></ListItemIcon>
|
||||
Create Report from Current View
|
||||
</MenuItem>
|
||||
</Tooltip>
|
||||
}
|
||||
{
|
||||
isQueryScreen && <Divider />
|
||||
isQueryScreen && <Divider/>
|
||||
}
|
||||
<MenuItem disabled style={{"opacity": "initial"}}><b>Your Saved Views</b></MenuItem>
|
||||
{
|
||||
yourSavedViews && yourSavedViews.length > 0 ? (
|
||||
yourSavedViews.map((record: QRecord, index: number) =>
|
||||
savedViews && savedViews.length > 0 ? (
|
||||
savedViews.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 saved views for this table.</i>
|
||||
</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>
|
||||
);
|
||||
|
||||
@ -566,7 +520,7 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
|
||||
let buttonBorder = colors.grayLines.main;
|
||||
let buttonColor = colors.gray.main;
|
||||
|
||||
if (currentSavedView)
|
||||
if(currentSavedView)
|
||||
{
|
||||
if (viewIsModified)
|
||||
{
|
||||
@ -594,23 +548,23 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
|
||||
color: buttonColor,
|
||||
backgroundColor: buttonBackground,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
function isSaveButtonDisabled(): boolean
|
||||
{
|
||||
if (isSubmitting)
|
||||
if(isSubmitting)
|
||||
{
|
||||
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);
|
||||
}
|
||||
@ -639,7 +593,7 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
|
||||
fontWeight: 500,
|
||||
fontSize: "0.875rem",
|
||||
p: "0.5rem",
|
||||
...buttonStyles
|
||||
... buttonStyles
|
||||
}}
|
||||
>
|
||||
<Icon sx={{mr: "0.5rem"}}>save</Icon>
|
||||
@ -670,7 +624,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>
|
||||
</>
|
||||
}
|
||||
{
|
||||
@ -681,20 +635,16 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
|
||||
{
|
||||
viewDiffs.map((s: string, i: number) => <li key={i}>{s}</li>)
|
||||
}
|
||||
</ul>
|
||||
{
|
||||
notOwnerTooltipText && <i>{notOwnerTooltipText}</i>
|
||||
}
|
||||
</>}>
|
||||
</ul></>}>
|
||||
<Box display="inline" sx={{...linkButtonStyle, p: 0, cursor: "default", position: "relative", top: "-1px"}}>{viewDiffs.length} Unsaved Change{viewDiffs.length == 1 ? "" : "s"}</Box>
|
||||
</Tooltip>
|
||||
|
||||
{disabledBecauseNotOwner ? <> </> : <Button disableRipple={true} sx={linkButtonStyle} onClick={() => handleDropdownOptionClick(SAVE_OPTION)}>Save…</Button>}
|
||||
<Button disableRipple={true} sx={linkButtonStyle} onClick={() => handleDropdownOptionClick(SAVE_OPTION)}>Save…</Button>
|
||||
|
||||
{/* vertical rule */}
|
||||
<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>
|
||||
</>
|
||||
}
|
||||
{
|
||||
@ -713,17 +663,16 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
|
||||
{
|
||||
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>
|
||||
</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 */}
|
||||
<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>
|
||||
@ -753,15 +702,15 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
|
||||
) : (
|
||||
isSaveFilterAs ? (
|
||||
<DialogTitle id="alert-dialog-title">Save View As</DialogTitle>
|
||||
) : (
|
||||
):(
|
||||
isRenameFilter ? (
|
||||
<DialogTitle id="alert-dialog-title">Rename View</DialogTitle>
|
||||
) : (
|
||||
):(
|
||||
<DialogTitle id="alert-dialog-title">Update Existing View</DialogTitle>
|
||||
)
|
||||
)
|
||||
)
|
||||
) : (
|
||||
):(
|
||||
<DialogTitle id="alert-dialog-title">Save New View</DialogTitle>
|
||||
)
|
||||
}
|
||||
@ -772,12 +721,12 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
|
||||
</Box>
|
||||
) : ("")}
|
||||
{
|
||||
(!currentSavedView || isSaveFilterAs || isRenameFilter) && !isDeleteFilter ? (
|
||||
(! currentSavedView || isSaveFilterAs || isRenameFilter) && ! isDeleteFilter ? (
|
||||
<Box>
|
||||
{
|
||||
isSaveFilterAs ? (
|
||||
<Box mb={3}>Enter a name for this new saved view.</Box>
|
||||
) : (
|
||||
):(
|
||||
<Box mb={3}>Enter a new name for this saved view.</Box>
|
||||
)
|
||||
}
|
||||
@ -795,10 +744,10 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
):(
|
||||
isDeleteFilter ? (
|
||||
<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>
|
||||
)
|
||||
)
|
||||
@ -810,7 +759,7 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
|
||||
isDeleteFilter ?
|
||||
<QDeleteButton onClickHandler={handleFilterDialogButtonOnClick} disabled={isSubmitting} />
|
||||
:
|
||||
<QSaveButton label="Save" onClickHandler={handleFilterDialogButtonOnClick} disabled={isSaveButtonDisabled()} />
|
||||
<QSaveButton label="Save" onClickHandler={handleFilterDialogButtonOnClick} disabled={isSaveButtonDisabled()}/>
|
||||
}
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
@ -1,482 +0,0 @@
|
||||
/*
|
||||
* 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 && (<> </>)}
|
||||
</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 ? <> ({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},
|
||||
};
|
||||
|
@ -49,13 +49,11 @@ import Tooltip from "@mui/material/Tooltip/Tooltip";
|
||||
import QContext from "QContext";
|
||||
import colors from "qqq/assets/theme/base/colors";
|
||||
import AuditBody from "qqq/components/audits/AuditBody";
|
||||
import {QActionsMenuButton, QCancelButton, QDeleteButton, QEditButton, standardWidth} from "qqq/components/buttons/DefaultButtons";
|
||||
import {QActionsMenuButton, QCancelButton, QDeleteButton, QEditButton} from "qqq/components/buttons/DefaultButtons";
|
||||
import EntityForm from "qqq/components/forms/EntityForm";
|
||||
import MDButton from "qqq/components/legacy/MDButton";
|
||||
import {GotoRecordButton} from "qqq/components/misc/GotoRecordDialog";
|
||||
import HelpContent, {hasHelpContent} from "qqq/components/misc/HelpContent";
|
||||
import QRecordSidebar from "qqq/components/misc/RecordSidebar";
|
||||
import ShareModal from "qqq/components/sharing/ShareModal";
|
||||
import DashboardWidgets from "qqq/components/widgets/DashboardWidgets";
|
||||
import BaseLayout from "qqq/layouts/BaseLayout";
|
||||
import ProcessRun from "qqq/pages/processes/ProcessRun";
|
||||
@ -84,10 +82,6 @@ RecordView.defaultProps =
|
||||
|
||||
const TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT = "qqq.tableVariant";
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Record View Screen component.
|
||||
*******************************************************************************/
|
||||
function RecordView({table, launchProcess}: Props): JSX.Element
|
||||
{
|
||||
const {id} = useParams();
|
||||
@ -123,14 +117,11 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
||||
const [launchingProcess, setLaunchingProcess] = useState(launchProcess);
|
||||
const [showEditChildForm, setShowEditChildForm] = useState(null as any);
|
||||
const [showAudit, setShowAudit] = useState(false);
|
||||
const [showShareModal, setShowShareModal] = useState(false);
|
||||
|
||||
const [isDeleteSubmitting, setIsDeleteSubmitting] = useState(false);
|
||||
|
||||
const openActionsMenu = (event: any) => setActionsMenu(event.currentTarget);
|
||||
const closeActionsMenu = () => setActionsMenu(null);
|
||||
|
||||
const {accentColor, setPageHeader, tableMetaData, setTableMetaData, tableProcesses, setTableProcesses, dotMenuOpen, keyboardHelpOpen, helpHelpActive, recordAnalytics, userId: currentUserId} = useContext(QContext);
|
||||
const {accentColor, setPageHeader, tableMetaData, setTableMetaData, tableProcesses, setTableProcesses, dotMenuOpen, keyboardHelpOpen, helpHelpActive, recordAnalytics} = useContext(QContext);
|
||||
|
||||
if (localStorage.getItem(tableVariantLocalStorageKey))
|
||||
{
|
||||
@ -631,7 +622,6 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
||||
const handleClickDeleteButton = () =>
|
||||
{
|
||||
setDeleteConfirmationOpen(true);
|
||||
setIsDeleteSubmitting(false);
|
||||
};
|
||||
|
||||
const handleDeleteConfirmClose = () =>
|
||||
@ -641,7 +631,6 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
||||
|
||||
const handleDelete = (event: { preventDefault: () => void }) =>
|
||||
{
|
||||
setIsDeleteSubmitting(true);
|
||||
event?.preventDefault();
|
||||
(async () =>
|
||||
{
|
||||
@ -650,13 +639,11 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
||||
await qController.delete(tableName, id)
|
||||
.then(() =>
|
||||
{
|
||||
setIsDeleteSubmitting(false);
|
||||
const path = pathParts.slice(0, -1).join("/");
|
||||
navigate(path, {state: {deleteSuccess: true}});
|
||||
})
|
||||
.catch((error) =>
|
||||
{
|
||||
setIsDeleteSubmitting(false);
|
||||
setDeleteConfirmationOpen(false);
|
||||
console.log("Caught:");
|
||||
console.log(error);
|
||||
@ -772,68 +759,6 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
||||
</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) =>
|
||||
{
|
||||
navigate(process.name);
|
||||
@ -989,7 +914,6 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
||||
</Typography>
|
||||
<Box display="flex">
|
||||
<GotoRecordButton metaData={metaData} tableMetaData={tableMetaData} />
|
||||
{renderShareButton()}
|
||||
<QActionsMenuButton isOpen={actionsMenu} onClickHandler={openActionsMenu} />
|
||||
</Box>
|
||||
{renderActionsMenu}
|
||||
@ -1038,7 +962,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleDeleteConfirmClose}>No</Button>
|
||||
<Button onClick={handleDelete} autoFocus disabled={isDeleteSubmitting}>
|
||||
<Button onClick={handleDelete} autoFocus>
|
||||
Yes
|
||||
</Button>
|
||||
</DialogActions>
|
||||
@ -1086,11 +1010,6 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
||||
</Modal>
|
||||
}
|
||||
|
||||
{
|
||||
showShareModal && tableMetaData && record &&
|
||||
<ShareModal open={showShareModal} onClose={closeShareModal} tableMetaData={tableMetaData} record={record}></ShareModal>
|
||||
}
|
||||
|
||||
</Box>
|
||||
}
|
||||
</Box>
|
||||
|
@ -47,8 +47,6 @@ module.exports = function (app)
|
||||
app.use("/download/*", getRequestHandler());
|
||||
app.use("/metaData/*", getRequestHandler());
|
||||
app.use("/data/*", getRequestHandler());
|
||||
app.use("/possibleValues/*", getRequestHandler());
|
||||
app.use("/possibleValues", getRequestHandler());
|
||||
app.use("/widget/*", getRequestHandler());
|
||||
app.use("/serverInfo", getRequestHandler());
|
||||
app.use("/manageSession", getRequestHandler());
|
||||
|
@ -261,9 +261,7 @@ public class QueryScreenLib
|
||||
if(StringUtils.hasContent(value))
|
||||
{
|
||||
qSeleniumLib.waitForSelector(".filterValuesColumn INPUT").click();
|
||||
// todo - no, not in a listbox/LI here...
|
||||
qSeleniumLib.waitForSelectorContaining(".MuiAutocomplete-listbox LI", value).click();
|
||||
System.out.println(value);
|
||||
qSeleniumLib.waitForSelector(".filterValuesColumn INPUT").sendKeys(value);
|
||||
}
|
||||
|
||||
qSeleniumLib.clickBackdrop();
|
||||
@ -271,6 +269,21 @@ public class QueryScreenLib
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void setBasicBooleanFilter(String fieldLabel, String operatorLabel)
|
||||
{
|
||||
qSeleniumLib.waitForSelectorContaining("BUTTON", fieldLabel).click();
|
||||
qSeleniumLib.waitForMillis(250);
|
||||
qSeleniumLib.waitForSelector("#criteriaOperator").click();
|
||||
qSeleniumLib.waitForSelectorContaining("LI", operatorLabel).click();
|
||||
|
||||
qSeleniumLib.clickBackdrop();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -0,0 +1,172 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.frontend.materialdashboard.selenium.tests;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.frontend.materialdashboard.selenium.lib.QBaseSeleniumTest;
|
||||
import com.kingsrook.qqq.frontend.materialdashboard.selenium.lib.QSeleniumLib;
|
||||
import com.kingsrook.qqq.frontend.materialdashboard.selenium.lib.QueryScreenLib;
|
||||
import com.kingsrook.qqq.frontend.materialdashboard.selenium.lib.javalin.QSeleniumJavalin;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.openqa.selenium.By;
|
||||
import org.openqa.selenium.Keys;
|
||||
import org.openqa.selenium.WebElement;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Test for Saved Report screen (table has some special behaviors)
|
||||
*******************************************************************************/
|
||||
public class SavedReportTest extends QBaseSeleniumTest
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected void addJavalinRoutes(QSeleniumJavalin qSeleniumJavalin)
|
||||
{
|
||||
super.addJavalinRoutes(qSeleniumJavalin);
|
||||
qSeleniumJavalin
|
||||
.withRouteToFile("/metaData/table/savedReport", "metaData/table/savedReport.json")
|
||||
.withRouteToFile("/widget/reportSetupWidget", "widget/reportSetupWidget.json")
|
||||
.withRouteToFile("/widget/pivotTableSetupWidget", "widget/pivotTableSetupWidget.json")
|
||||
.withRouteToFile("/data/savedReport/possibleValues/tableName", "data/savedReport/possibleValues/tableName.json")
|
||||
|
||||
.withRouteToFile("/data/person/count", "data/person/count.json")
|
||||
.withRouteToFile("/data/person/query", "data/person/index.json")
|
||||
;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testCreate()
|
||||
{
|
||||
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/userCustomizations/savedReport/create", "Creating New Saved Report");
|
||||
|
||||
//////////////////////////////////////////////////////////////
|
||||
// make sure things are disabled before a table is selected //
|
||||
//////////////////////////////////////////////////////////////
|
||||
WebElement webElement = qSeleniumLib.waitForSelectorContaining("button", "Edit Filters and Columns");
|
||||
assertEquals("true", webElement.getAttribute("disabled"));
|
||||
|
||||
qSeleniumLib.waitForSelector("#label").click();
|
||||
qSeleniumLib.waitForSelector("#label").sendKeys("My Report");
|
||||
|
||||
qSeleniumLib.waitForSelector("#tableName").click();
|
||||
qSeleniumLib.waitForSelector("#tableName").sendKeys("Person" + Keys.DOWN + Keys.ENTER);
|
||||
|
||||
//////////////////////////////////
|
||||
// make sure things enabled now //
|
||||
//////////////////////////////////
|
||||
webElement = qSeleniumLib.waitForSelectorContaining("button", "Edit Filters and Columns");
|
||||
assertNull(webElement.getAttribute("disabled"));
|
||||
|
||||
////////////////////////////////////////////////////
|
||||
// open query-screen popup, wait for query to run //
|
||||
////////////////////////////////////////////////////
|
||||
qSeleniumJavalin.beginCapture();
|
||||
qSeleniumLib.waitForSelectorContaining("button", "Edit Filters and Columns").click();
|
||||
qSeleniumJavalin.waitForCapturedPath("/data/person/count");
|
||||
qSeleniumJavalin.waitForCapturedPath("/data/person/query");
|
||||
qSeleniumJavalin.endCapture();
|
||||
|
||||
QueryScreenLib queryScreenLib = new QueryScreenLib(qSeleniumLib);
|
||||
queryScreenLib.setBasicFilter("First Name", "contains", "Darin");
|
||||
|
||||
////////////////////////
|
||||
// close query screen //
|
||||
////////////////////////
|
||||
qSeleniumLib.waitForSelectorContaining("button", "OK").click();
|
||||
|
||||
//////////////////////////////////////////////////////
|
||||
// make sure query things appear on edit screen now //
|
||||
//////////////////////////////////////////////////////
|
||||
qSeleniumLib.waitForSelectorContaining(".advancedQueryString", "First Name");
|
||||
qSeleniumLib.waitForSelectorContaining(".advancedQueryString", "contains");
|
||||
qSeleniumLib.waitForSelectorContaining(".advancedQueryString", "Darin");
|
||||
List<WebElement> columns = qSeleniumLib.waitForSelectorContaining("h5", "Columns")
|
||||
.findElement(QSeleniumLib.PARENT)
|
||||
.findElements(By.cssSelector("DIV"));
|
||||
|
||||
assertThat(columns)
|
||||
.hasSizeGreaterThanOrEqualTo(5) // at least this many
|
||||
.anyMatch(we -> we.getText().equals("Home City")); // a few fields are found
|
||||
|
||||
///////////////////
|
||||
// turn on pivot //
|
||||
///////////////////
|
||||
qSeleniumLib.waitForSelectorContaining("label", "Use Pivot Table").click();
|
||||
qSeleniumLib.waitForSelectorContaining("button", "Edit Pivot Table").click();
|
||||
qSeleniumLib.waitForSelectorContaining("h3", "Edit Pivot Table");
|
||||
|
||||
///////////////
|
||||
// add a row //
|
||||
///////////////
|
||||
qSeleniumLib.waitForSelectorContaining(".MuiModal-root button", "Add new row").click();
|
||||
WebElement row0Input = qSeleniumLib.waitForSelector("#rows-0");
|
||||
row0Input.click();
|
||||
row0Input.sendKeys("Last Name" + Keys.ENTER);
|
||||
|
||||
//////////////////
|
||||
// add a column //
|
||||
//////////////////
|
||||
qSeleniumLib.waitForSelectorContaining(".MuiModal-root button", "Add new column").click();
|
||||
WebElement column0Input = qSeleniumLib.waitForSelector("#columns-0");
|
||||
column0Input.click();
|
||||
column0Input.sendKeys("Home City" + Keys.ENTER);
|
||||
|
||||
/////////////////
|
||||
// add a value //
|
||||
/////////////////
|
||||
qSeleniumLib.waitForSelectorContaining(".MuiModal-root button", "Add new value").click();
|
||||
WebElement value0Input = qSeleniumLib.waitForSelector("#values-field-0");
|
||||
value0Input.click();
|
||||
value0Input.sendKeys("Id" + Keys.ENTER);
|
||||
|
||||
/////////////////////////////////////////
|
||||
// try to submit - but expect an error //
|
||||
/////////////////////////////////////////
|
||||
qSeleniumLib.waitForSelectorContaining("button", "OK").click();
|
||||
qSeleniumLib.waitForSelectorContaining(".MuiAlert-standard", "Missing value in 1 field.").click();
|
||||
|
||||
///////////////////////////
|
||||
// now select a function //
|
||||
///////////////////////////
|
||||
WebElement function0Input = qSeleniumLib.waitForSelector("#values-function-0");
|
||||
function0Input.click();
|
||||
function0Input.sendKeys("Count" + Keys.ENTER);
|
||||
|
||||
qSeleniumLib.waitForSelectorContaining("button", "OK").click();
|
||||
qSeleniumLib.waitForSelectorContainingToNotExist("h3", "Edit Pivot Table");
|
||||
|
||||
// qSeleniumLib.waitForever();
|
||||
}
|
||||
|
||||
}
|
@ -153,16 +153,16 @@ public class QueryScreenTest extends QBaseSeleniumTest
|
||||
|
||||
queryScreenLib.addBasicFilter("Is Employed");
|
||||
|
||||
testBasicCriteria(queryScreenLib, "Is Employed", "equals yes", null, "(?s).*Is Employed:.*yes.*", """
|
||||
testBasicBooleanCriteria(queryScreenLib, "Is Employed", "equals yes", "(?s).*Is Employed:.*yes.*", """
|
||||
{"fieldName":"isEmployed","operator":"EQUALS","values":[true]}""");
|
||||
|
||||
testBasicCriteria(queryScreenLib, "Is Employed", "equals no", null, "(?s).*Is Employed:.*no.*", """
|
||||
testBasicBooleanCriteria(queryScreenLib, "Is Employed", "equals no", "(?s).*Is Employed:.*no.*", """
|
||||
{"fieldName":"isEmployed","operator":"EQUALS","values":[false]}""");
|
||||
|
||||
testBasicCriteria(queryScreenLib, "Is Employed", "is empty", null, "(?s).*Is Employed:.*is empty.*", """
|
||||
testBasicBooleanCriteria(queryScreenLib, "Is Employed", "is empty", "(?s).*Is Employed:.*is empty.*", """
|
||||
{"fieldName":"isEmployed","operator":"IS_BLANK","values":[]}""");
|
||||
|
||||
testBasicCriteria(queryScreenLib, "Is Employed", "is not empty", null, "(?s).*Is Employed:.*is not empty.*", """
|
||||
testBasicBooleanCriteria(queryScreenLib, "Is Employed", "is not empty", "(?s).*Is Employed:.*is not empty.*", """
|
||||
{"fieldName":"isEmployed","operator":"IS_NOT_BLANK","values":[]}""");
|
||||
}
|
||||
|
||||
@ -203,10 +203,10 @@ public class QueryScreenTest extends QBaseSeleniumTest
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void testBasicCriteria(QueryScreenLib queryScreenLib, String fieldLabel, String operatorLabel, String value, String expectButtonStringRegex, String expectFilterJsonContains)
|
||||
private void testBasicBooleanCriteria(QueryScreenLib queryScreenLib, String fieldLabel, String operatorLabel, String expectButtonStringRegex, String expectFilterJsonContains)
|
||||
{
|
||||
qSeleniumJavalin.beginCapture();
|
||||
queryScreenLib.setBasicFilter(fieldLabel, operatorLabel, value);
|
||||
queryScreenLib.setBasicBooleanFilter(fieldLabel, operatorLabel);
|
||||
queryScreenLib.waitForBasicFilterButtonMatchingRegex(expectButtonStringRegex);
|
||||
qSeleniumJavalin.waitForCapturedPathWithBodyContaining("/data/person/query", expectFilterJsonContains);
|
||||
qSeleniumJavalin.endCapture();
|
||||
|
@ -0,0 +1,16 @@
|
||||
{
|
||||
"options": [
|
||||
{
|
||||
"id": "person",
|
||||
"label": "Person"
|
||||
},
|
||||
{
|
||||
"id": "city",
|
||||
"label": "City"
|
||||
},
|
||||
{
|
||||
"id": "savedReport",
|
||||
"label": "Saved Report"
|
||||
}
|
||||
]
|
||||
}
|
@ -189,6 +189,26 @@
|
||||
"insertPermission": true,
|
||||
"editPermission": true,
|
||||
"deletePermission": true
|
||||
},
|
||||
"savedReport": {
|
||||
"name": "savedReport",
|
||||
"label": "Saved Report",
|
||||
"isHidden": false,
|
||||
"iconName": "article",
|
||||
"capabilities": [
|
||||
"TABLE_COUNT",
|
||||
"TABLE_GET",
|
||||
"TABLE_QUERY",
|
||||
"QUERY_STATS",
|
||||
"TABLE_UPDATE",
|
||||
"TABLE_INSERT",
|
||||
"TABLE_DELETE"
|
||||
],
|
||||
"readPermission": true,
|
||||
"insertPermission": true,
|
||||
"editPermission": true,
|
||||
"deletePermission": true,
|
||||
"usesVariants": false
|
||||
}
|
||||
},
|
||||
"processes": {
|
||||
@ -420,6 +440,40 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"userCustomizations": {
|
||||
"name": "userCustomizations",
|
||||
"label": "User Customizations",
|
||||
"iconName": "article",
|
||||
"widgets": [],
|
||||
"children": [
|
||||
{
|
||||
"type": "TABLE",
|
||||
"name": "savedReport",
|
||||
"label": "Saved Report",
|
||||
"iconName": "article"
|
||||
}
|
||||
],
|
||||
"childMap": {
|
||||
"savedReport": {
|
||||
"type": "TABLE",
|
||||
"name": "savedReport",
|
||||
"label": "Saved Report",
|
||||
"iconName": "article"
|
||||
}
|
||||
},
|
||||
"sections": [
|
||||
{
|
||||
"name": "userCustomizations",
|
||||
"label": "User Customizations",
|
||||
"icon": {
|
||||
"name": "badge"
|
||||
},
|
||||
"tables": [
|
||||
"savedReport"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"miscellaneous": {
|
||||
"name": "miscellaneous",
|
||||
"label": "Miscellaneous",
|
||||
@ -730,6 +784,20 @@
|
||||
}
|
||||
],
|
||||
"iconName": "data_object"
|
||||
},
|
||||
{
|
||||
"type": "APP",
|
||||
"name": "userCustomizations",
|
||||
"label": "User Customizations",
|
||||
"children": [
|
||||
{
|
||||
"type": "TABLE",
|
||||
"name": "savedReport",
|
||||
"label": "Saved Report",
|
||||
"iconName": "article"
|
||||
}
|
||||
],
|
||||
"iconName": "data_object"
|
||||
}
|
||||
],
|
||||
"branding": {
|
||||
@ -750,6 +818,28 @@
|
||||
"isCard": true,
|
||||
"storeDropdownSelections": false,
|
||||
"hasPermission": true
|
||||
},
|
||||
"reportSetupWidget": {
|
||||
"name": "reportSetupWidget",
|
||||
"label": "Filters and Columns",
|
||||
"type": "reportSetup",
|
||||
"isCard": true,
|
||||
"storeDropdownSelections": false,
|
||||
"showReloadButton": true,
|
||||
"showExportButton": false,
|
||||
"defaultValues": {},
|
||||
"hasPermission": true
|
||||
},
|
||||
"pivotTableSetupWidget": {
|
||||
"name": "pivotTableSetupWidget",
|
||||
"label": "Pivot Table",
|
||||
"type": "pivotTableSetup",
|
||||
"isCard": true,
|
||||
"storeDropdownSelections": false,
|
||||
"showReloadButton": true,
|
||||
"showExportButton": false,
|
||||
"defaultValues": {},
|
||||
"hasPermission": true
|
||||
}
|
||||
},
|
||||
"environmentValues": {
|
||||
|
218
src/test/resources/fixtures/metaData/table/savedReport.json
Normal file
218
src/test/resources/fixtures/metaData/table/savedReport.json
Normal file
@ -0,0 +1,218 @@
|
||||
{
|
||||
"table": {
|
||||
"name": "savedReport",
|
||||
"label": "Saved Report",
|
||||
"isHidden": false,
|
||||
"primaryKeyField": "id",
|
||||
"iconName": "article",
|
||||
"fields": {
|
||||
"queryFilterJson": {
|
||||
"name": "queryFilterJson",
|
||||
"label": "Query Filter",
|
||||
"type": "STRING",
|
||||
"isRequired": false,
|
||||
"isEditable": true,
|
||||
"isHeavy": false,
|
||||
"displayFormat": "%s"
|
||||
},
|
||||
"columnsJson": {
|
||||
"name": "columnsJson",
|
||||
"label": "Columns",
|
||||
"type": "STRING",
|
||||
"isRequired": false,
|
||||
"isEditable": true,
|
||||
"isHeavy": false,
|
||||
"displayFormat": "%s"
|
||||
},
|
||||
"inputFieldsJson": {
|
||||
"name": "inputFieldsJson",
|
||||
"label": "Input Fields",
|
||||
"type": "STRING",
|
||||
"isRequired": false,
|
||||
"isEditable": true,
|
||||
"isHeavy": false,
|
||||
"displayFormat": "%s"
|
||||
},
|
||||
"pivotTableJson": {
|
||||
"name": "pivotTableJson",
|
||||
"label": "Pivot Table",
|
||||
"type": "STRING",
|
||||
"isRequired": false,
|
||||
"isEditable": true,
|
||||
"isHeavy": false,
|
||||
"displayFormat": "%s"
|
||||
},
|
||||
"modifyDate": {
|
||||
"name": "modifyDate",
|
||||
"label": "Modify Date",
|
||||
"type": "DATE_TIME",
|
||||
"isRequired": false,
|
||||
"isEditable": false,
|
||||
"isHeavy": false,
|
||||
"displayFormat": "%s"
|
||||
},
|
||||
"label": {
|
||||
"name": "label",
|
||||
"label": "Report Name",
|
||||
"type": "STRING",
|
||||
"isRequired": true,
|
||||
"isEditable": true,
|
||||
"isHeavy": false,
|
||||
"displayFormat": "%s"
|
||||
},
|
||||
"id": {
|
||||
"name": "id",
|
||||
"label": "Id",
|
||||
"type": "INTEGER",
|
||||
"isRequired": false,
|
||||
"isEditable": false,
|
||||
"isHeavy": false,
|
||||
"displayFormat": "%s"
|
||||
},
|
||||
"userId": {
|
||||
"name": "userId",
|
||||
"label": "User Id",
|
||||
"type": "STRING",
|
||||
"isRequired": false,
|
||||
"isEditable": true,
|
||||
"isHeavy": false,
|
||||
"displayFormat": "%s"
|
||||
},
|
||||
"tableName": {
|
||||
"name": "tableName",
|
||||
"label": "Table",
|
||||
"type": "STRING",
|
||||
"isRequired": true,
|
||||
"isEditable": true,
|
||||
"isHeavy": false,
|
||||
"possibleValueSourceName": "tables",
|
||||
"displayFormat": "%s"
|
||||
},
|
||||
"createDate": {
|
||||
"name": "createDate",
|
||||
"label": "Create Date",
|
||||
"type": "DATE_TIME",
|
||||
"isRequired": false,
|
||||
"isEditable": false,
|
||||
"isHeavy": false,
|
||||
"displayFormat": "%s"
|
||||
}
|
||||
},
|
||||
"sections": [
|
||||
{
|
||||
"name": "identity",
|
||||
"label": "Identity",
|
||||
"tier": "T1",
|
||||
"fieldNames": [
|
||||
"id",
|
||||
"label",
|
||||
"tableName"
|
||||
],
|
||||
"icon": {
|
||||
"name": "badge"
|
||||
},
|
||||
"isHidden": false
|
||||
},
|
||||
{
|
||||
"name": "filtersAndColumns",
|
||||
"label": "Filters and Columns",
|
||||
"tier": "T2",
|
||||
"widgetName": "reportSetupWidget",
|
||||
"icon": {
|
||||
"name": "table_chart"
|
||||
},
|
||||
"isHidden": false
|
||||
},
|
||||
{
|
||||
"name": "pivotTable",
|
||||
"label": "Pivot Table",
|
||||
"tier": "T2",
|
||||
"widgetName": "pivotTableSetupWidget",
|
||||
"icon": {
|
||||
"name": "pivot_table_chart"
|
||||
},
|
||||
"isHidden": false
|
||||
},
|
||||
{
|
||||
"name": "data",
|
||||
"label": "Data",
|
||||
"tier": "T2",
|
||||
"fieldNames": [
|
||||
"queryFilterJson",
|
||||
"columnsJson",
|
||||
"pivotTableJson"
|
||||
],
|
||||
"icon": {
|
||||
"name": "text_snippet"
|
||||
},
|
||||
"isHidden": true
|
||||
},
|
||||
{
|
||||
"name": "hidden",
|
||||
"label": "Hidden",
|
||||
"tier": "T2",
|
||||
"fieldNames": [
|
||||
"inputFieldsJson",
|
||||
"userId"
|
||||
],
|
||||
"icon": {
|
||||
"name": "text_snippet"
|
||||
},
|
||||
"isHidden": true
|
||||
},
|
||||
{
|
||||
"name": "dates",
|
||||
"label": "Dates",
|
||||
"tier": "T3",
|
||||
"fieldNames": [
|
||||
"createDate",
|
||||
"modifyDate"
|
||||
],
|
||||
"icon": {
|
||||
"name": "calendar_month"
|
||||
},
|
||||
"isHidden": false
|
||||
}
|
||||
],
|
||||
"exposedJoins": [],
|
||||
"supplementalTableMetaData": {
|
||||
"materialDashboard": {
|
||||
"fieldRules": [
|
||||
{
|
||||
"trigger": "ON_CHANGE",
|
||||
"sourceField": "tableName",
|
||||
"action": "CLEAR_TARGET_FIELD",
|
||||
"targetField": "queryFilterJson"
|
||||
},
|
||||
{
|
||||
"trigger": "ON_CHANGE",
|
||||
"sourceField": "tableName",
|
||||
"action": "CLEAR_TARGET_FIELD",
|
||||
"targetField": "columnsJson"
|
||||
},
|
||||
{
|
||||
"trigger": "ON_CHANGE",
|
||||
"sourceField": "tableName",
|
||||
"action": "CLEAR_TARGET_FIELD",
|
||||
"targetField": "pivotTableJson"
|
||||
}
|
||||
],
|
||||
"type": "materialDashboard"
|
||||
}
|
||||
},
|
||||
"capabilities": [
|
||||
"TABLE_COUNT",
|
||||
"TABLE_GET",
|
||||
"TABLE_QUERY",
|
||||
"QUERY_STATS",
|
||||
"TABLE_UPDATE",
|
||||
"TABLE_INSERT",
|
||||
"TABLE_DELETE"
|
||||
],
|
||||
"readPermission": true,
|
||||
"insertPermission": true,
|
||||
"editPermission": true,
|
||||
"deletePermission": true,
|
||||
"usesVariants": false
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
{"type":"pivotTableSetup"}
|
@ -0,0 +1 @@
|
||||
{"type":"reportSetup"}
|
Reference in New Issue
Block a user