Compare commits

..

6 Commits

6 changed files with 18352 additions and 498 deletions

18466
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -664,7 +664,13 @@ 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] = useState(user.email); const [userId, setUserId] = useState(user?.email);
useEffect(() =>
{
setUserId(user?.email)
}, [user]);
const [googleAnalyticsUtils] = useState(new GoogleAnalyticsUtils()); const [googleAnalyticsUtils] = useState(new GoogleAnalyticsUtils());

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,7 +115,7 @@ 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;
@ -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);
}
/******************************************************************************* /*******************************************************************************
@ -159,7 +176,6 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
}; };
/******************************************************************************* /*******************************************************************************
** 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,7 +187,7 @@ 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)
{ {
@ -186,7 +202,7 @@ 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)
{ {
@ -201,13 +217,13 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
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;
} }
} };
/******************************************************************************* /*******************************************************************************
@ -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
*******************************************************************************/ *******************************************************************************/
@ -267,7 +282,7 @@ 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));
@ -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
{ {
////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////
@ -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,56 +463,68 @@ 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>
} }
{ {
@ -500,8 +532,8 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
} }
<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>
@ -512,6 +544,20 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
</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>
); );
@ -548,7 +594,7 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
color: buttonColor, color: buttonColor,
backgroundColor: buttonBackground, backgroundColor: buttonBackground,
} }
} };
/******************************************************************************* /*******************************************************************************
** **
@ -560,7 +606,7 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
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)
{ {
@ -635,11 +681,15 @@ 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" />
@ -663,7 +713,8 @@ 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>

View File

@ -37,6 +37,7 @@ import Typography from "@mui/material/Typography";
import FormData from "form-data"; import FormData from "form-data";
import colors from "qqq/assets/theme/base/colors"; import colors from "qqq/assets/theme/base/colors";
import {QCancelButton} from "qqq/components/buttons/DefaultButtons"; import {QCancelButton} from "qqq/components/buttons/DefaultButtons";
import DynamicSelect, {getAutocompleteOutlinedStyle} from "qqq/components/forms/DynamicSelect";
import Client from "qqq/utils/qqq/Client"; import Client from "qqq/utils/qqq/Client";
import React, {useEffect, useReducer, useState} from "react"; import React, {useEffect, useReducer, useState} from "react";
@ -75,6 +76,17 @@ const defaultScope = scopeOptions[0];
const qController = Client.getInstance(); 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 ** component containing a Modal dialog for sharing records
*******************************************************************************/ *******************************************************************************/
@ -83,6 +95,7 @@ export default function ShareModal({open, onClose, tableMetaData, record}: Share
const [statusString, setStatusString] = useState("Loading..."); const [statusString, setStatusString] = useState("Loading...");
const [alert, setAlert] = useState(null as string); const [alert, setAlert] = useState(null as string);
const [selectedAudienceOption, setSelectedAudienceOption] = useState(null as {id: string, label: string});
const [selectedAudienceType, setSelectedAudienceType] = useState(null); const [selectedAudienceType, setSelectedAudienceType] = useState(null);
const [selectedAudienceId, setSelectedAudienceId] = useState(null); const [selectedAudienceId, setSelectedAudienceId] = useState(null);
const [selectedScopeId, setSelectedScopeId] = useState(defaultScope.id); const [selectedScopeId, setSelectedScopeId] = useState(defaultScope.id);
@ -92,8 +105,14 @@ export default function ShareModal({open, onClose, tableMetaData, record}: Share
const [needToLoadCurrentShares, setNeedToLoadCurrentShares] = useState(true); const [needToLoadCurrentShares, setNeedToLoadCurrentShares] = useState(true);
const [everLoadedCurrentShares, setEverLoadedCurrentShares] = useState(false); const [everLoadedCurrentShares, setEverLoadedCurrentShares] = useState(false);
const shareableTableMetaData = tableMetaData.shareableTableMetaData as ShareableTableMetaData;
const [, forceUpdate] = useReducer((x) => x + 1, 0); 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 // // trigger initial load, and post any changes, re-load //
@ -124,7 +143,7 @@ export default function ShareModal({open, onClose, tableMetaData, record}: Share
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
function handleAudienceChange(event: React.SyntheticEvent, value: any | any[], reason: string) function handleAudienceChange(value: any | any[], reason: string)
{ {
if(value) if(value)
{ {
@ -260,6 +279,7 @@ export default function ShareModal({open, onClose, tableMetaData, record}: Share
const result = processResult as QJobComplete; const result = processResult as QJobComplete;
setStatusString(null); setStatusString(null);
setAlert(null); setAlert(null);
setSelectedAudienceOption(null);
setNeedToLoadCurrentShares(true); setNeedToLoadCurrentShares(true);
setSubmitting(false) setSubmitting(false)
} }
@ -297,16 +317,6 @@ export default function ShareModal({open, onClose, tableMetaData, record}: Share
} }
// todo - need this to be real
const audienceOptions = [
{id: "user:1", label: "Darin Kelkhoff"},
{id: "user:2", label: "Tom Chutterloin"},
{id: "user:3", label: "Tylers Ample"},
{id: "user:4", label: "Mames Mames"},
{id: "group:2", label: "Cold Track Engineering"}
];
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
@ -329,10 +339,11 @@ export default function ShareModal({open, onClose, tableMetaData, record}: Share
*******************************************************************************/ *******************************************************************************/
function renderScopeDropdown(id: string, defaultValue: Scope, onChange: (event: React.SyntheticEvent, value: any | any[], reason: string) => void) function renderScopeDropdown(id: string, defaultValue: Scope, onChange: (event: React.SyntheticEvent, value: any | any[], reason: string) => void)
{ {
const isDisabled = (id == "new-share-scope" && submitting);
return ( return (
<Autocomplete <Autocomplete
id={id} id={id}
disabled={id == "new-share-scope" && submitting} disabled={isDisabled}
renderInput={(params) => (<TextField {...params} label="Scope" variant="outlined" autoComplete="off" type="search" InputProps={{...params.InputProps}} />)} renderInput={(params) => (<TextField {...params} label="Scope" variant="outlined" autoComplete="off" type="search" InputProps={{...params.InputProps}} />)}
options={scopeOptions} options={scopeOptions}
// @ts-ignore // @ts-ignore
@ -345,7 +356,7 @@ export default function ShareModal({open, onClose, tableMetaData, record}: Share
autoHighlight={true} autoHighlight={true}
disableClearable disableClearable
fullWidth fullWidth
sx={autocompleteSX} sx={getAutocompleteOutlinedStyle(isDisabled)}
/> />
); );
} }
@ -369,7 +380,8 @@ export default function ShareModal({open, onClose, tableMetaData, record}: Share
</Box> </Box>
<Box fontSize={14} maxWidth="590px" pb={1} fontWeight="300"> <Box fontSize={14} maxWidth="590px" pb={1} fontWeight="300">
{alert && <Alert color="error" onClose={() => setAlert(null)}>{alert}</Alert>} {alert && <Alert color="error" onClose={() => setAlert(null)}>{alert}</Alert>}
{statusString}&nbsp; {statusString}
{!alert && !statusString && (<>&nbsp;</>)}
</Box> </Box>
</Typography> </Typography>
</Box> </Box>
@ -378,21 +390,14 @@ export default function ShareModal({open, onClose, tableMetaData, record}: Share
<Box pb={3} display="flex" flexDirection="column"> <Box pb={3} display="flex" flexDirection="column">
{/* row for adding a new share */} {/* row for adding a new share */}
<Box display="flex" flexDirection="row" alignItems="center"> <Box display="flex" flexDirection="row" alignItems="center">
<Box width="350px" pr={2}> <Box width="350px" pr={2} mb={-1.5}>
<Autocomplete <DynamicSelect
id="new-share-audience" possibleValueSourceName={shareableTableMetaData.audiencePossibleValueSourceName}
disabled={submitting} fieldLabel="User or Group" // todo should come from shareableTableMetaData
renderInput={(params) => (<TextField {...params} label="User or Group" variant="outlined" autoComplete="off" type="search" InputProps={{...params.InputProps}} />)} initialValue={selectedAudienceOption?.id}
options={audienceOptions} initialDisplayValue={selectedAudienceOption?.label}
inForm={false}
onChange={handleAudienceChange} onChange={handleAudienceChange}
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={autocompleteSX}
/> />
</Box> </Box>
<Box width="180px" pr={2}> <Box width="180px" pr={2}>
@ -418,10 +423,11 @@ export default function ShareModal({open, onClose, tableMetaData, record}: Share
} }
</h5> </h5>
</Box> </Box>
<Box sx={{border: `1px solid ${colors.grayLines.main}`, borderRadius: "1rem", overflow: "auto"}} height="180px" pt="0.5rem"> <Box sx={{border: `1px solid ${colors.grayLines.main}`, borderRadius: "1rem", overflow: "hidden"}}>
<Box sx={{overflow: "auto"}} height="210px" pt="0.75rem">
{ {
currentShares.map((share) => ( currentShares.map((share) => (
<Box key={share.shareId} display="flex" justifyContent="space-between" alignItems="center" p="0.25rem" fontSize="1rem"> <Box key={share.shareId} display="flex" justifyContent="space-between" alignItems="center" p="0.25rem" pb="0.75rem" fontSize="1rem">
<Box display="flex" alignItems="center"> <Box display="flex" alignItems="center">
<Box width="310px" pl="1rem">{share.audienceLabel}</Box> <Box width="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 width="160px">{renderScopeDropdown(`scope-${share.shareId}`, getScopeOption(share.scopeId), (event: React.SyntheticEvent, value: any | any[], reason: string) => editingExistingShareScope(share.shareId, value))}</Box>
@ -434,6 +440,7 @@ export default function ShareModal({open, onClose, tableMetaData, record}: Share
} }
</Box> </Box>
</Box> </Box>
</Box>
</Box> </Box>
@ -448,12 +455,6 @@ export default function ShareModal({open, onClose, tableMetaData, record}: Share
} }
const autocompleteSX =
{
"& .MuiAutocomplete-input": {padding: "0.125rem 0.5rem !important"},
"& .MuiOutlinedInput-root": {borderRadius: "0.75rem !important"}
};
const iconButtonSX = const iconButtonSX =
{ {
border: `1px solid ${colors.grayLines.main} !important`, border: `1px solid ${colors.grayLines.main} !important`,

View File

@ -130,7 +130,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element
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))
{ {
@ -796,13 +796,37 @@ function RecordView({table, launchProcess}: Props): JSX.Element
*******************************************************************************/ *******************************************************************************/
const renderShareButton = () => const renderShareButton = () =>
{ {
if (tableMetaData && (tableMetaData.name == "savedReport" || tableMetaData.name == "savedView")) // todo - not just based on name if (tableMetaData && tableMetaData.shareableTableMetaData)
{ {
const shareDisabled = false; // todo - only share if you're the owner? or do that in the modal? let shareDisabled = true;
return (<Box width={standardWidth} mr={3}> let disabledTooltipText = "";
<MDButton id="shareButton" type="button" color="info" size="small" onClick={() => openShareModal()} fullWidth startIcon={<Icon>share</Icon>} disabled={shareDisabled}> 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 Share
</MDButton> </MDButton>
</span>
</Tooltip>
</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());