diff --git a/src/qqq/components/horseshoe/Breadcrumbs.tsx b/src/qqq/components/horseshoe/Breadcrumbs.tsx index cc396bd..5322dcb 100644 --- a/src/qqq/components/horseshoe/Breadcrumbs.tsx +++ b/src/qqq/components/horseshoe/Breadcrumbs.tsx @@ -94,16 +94,19 @@ function QBreadcrumbs({icon, title, route, light}: Props): JSX.Element { //////////////////////////////////////////////////////// // avoid showing "saved view" as a breadcrumb element // + // e.g., if at /app/table/savedView/1 (so where i==2) // //////////////////////////////////////////////////////// - if(routes[i] === "savedView") + if(routes[i] === "savedView" && i == 2) { continue; } /////////////////////////////////////////////////////////////////////// // avoid showing the table name if it's the element before savedView // + // e.g., when at /app/table/savedView/1 (so where i==1) // + // we want to just be showing "App" // /////////////////////////////////////////////////////////////////////// - if(i < routes.length - 1 && routes[i+1] == "savedView") + if(i < routes.length - 1 && routes[i+1] == "savedView" && i == 1) { continue; } diff --git a/src/qqq/components/misc/SavedViews.tsx b/src/qqq/components/misc/SavedViews.tsx index 668cea1..32c7c6c 100644 --- a/src/qqq/components/misc/SavedViews.tsx +++ b/src/qqq/components/misc/SavedViews.tsx @@ -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 {QJobError} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobError"; import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord"; -import {Alert, Button} from "@mui/material"; -import Box from "@mui/material/Box"; +import {Alert, Box, Button} from "@mui/material"; import Dialog from "@mui/material/Dialog"; import DialogActions from "@mui/material/DialogActions"; import DialogContent from "@mui/material/DialogContent"; @@ -60,7 +59,7 @@ interface Props view?: RecordQueryView; viewAsJson?: string; viewOnChangeCallback?: (selectedSavedViewId: number) => void; - loadingSavedView: boolean + loadingSavedView: boolean; queryScreenUsage: QueryScreenUsage; } @@ -69,6 +68,8 @@ 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); @@ -91,7 +92,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} = useContext(QContext); + const {accentColor, accentColorLight, userId: currentUserId} = useContext(QContext); ///////////////////////////////////////////////////////////////////////////////////////// // this component is used by - but that component has different usages - // @@ -114,13 +115,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; } @@ -130,7 +131,7 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab *******************************************************************************/ async function loadSavedViews() { - if (! tableMetaData) + if (!tableMetaData) { return; } @@ -140,8 +141,24 @@ 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); + } /******************************************************************************* @@ -152,14 +169,13 @@ 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 *******************************************************************************/ @@ -171,12 +187,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(""); } @@ -186,28 +202,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; } - } + }; /******************************************************************************* @@ -215,11 +231,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); @@ -227,7 +243,6 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab } - /******************************************************************************* ** fired when save or delete button saved on confirmation dialogs *******************************************************************************/ @@ -247,7 +262,7 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab setSaveFilterPopupOpen(false); setSaveOptionsOpen(false); - await(async() => + await (async () => { handleDropdownOptionClick(CLEAR_OPTION); })(); @@ -267,14 +282,14 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab //////////////////////////////////////////////////////////////////////////// // strip away incomplete filters too, just for cleaner saved view filters // //////////////////////////////////////////////////////////////////////////// - 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")); } @@ -285,7 +300,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) { @@ -302,11 +317,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; } @@ -321,7 +336,6 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab } - /******************************************************************************* ** hides/shows the save options *******************************************************************************/ @@ -331,7 +345,6 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab }; - /******************************************************************************* ** 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 *******************************************************************************/ @@ -356,7 +368,6 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab }; - /******************************************************************************* ** closes current dialog *******************************************************************************/ @@ -366,7 +377,6 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab }; - /******************************************************************************* ** make a request to the backend for various savedView processes *******************************************************************************/ @@ -375,7 +385,7 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab ///////////////////////// // fetch saved filters // ///////////////////////// - let savedViews = [] as QRecord[] + let savedViews = [] as QRecord[]; try { ////////////////////////////////////////////////////////////////// @@ -386,12 +396,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++) { @@ -403,7 +413,7 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab } catch (e) { - throw(e); + throw (e); } return (savedViews); @@ -416,17 +426,27 @@ 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 && ( Save your current filters, columns and settings, for quick re-use at a later time.

You will be prompted to enter a name if you choose this option.}> - handleDropdownOptionClick(SAVE_OPTION)}> - save - {currentSavedView ? "Save..." : "Save As..."} - + Save your current filters, columns and settings, for quick re-use at a later time.

You will be prompted to enter a name if you choose this option.}> + + handleDropdownOptionClick(SAVE_OPTION)}> + save + {currentSavedView ? "Save..." : "Save As..."} + +
} { isQueryScreen && hasStorePermission && currentSavedView != null && - - handleDropdownOptionClick(RENAME_OPTION)}> - edit - Rename... - + + + handleDropdownOptionClick(RENAME_OPTION)}> + edit + Rename... + + } { isQueryScreen && hasStorePermission && currentSavedView != null && - handleDropdownOptionClick(DUPLICATE_OPTION)}> - content_copy - Save As... - + + handleDropdownOptionClick(DUPLICATE_OPTION)}> + content_copy + Save As... + + } { isQueryScreen && hasDeletePermission && currentSavedView != null && - - handleDropdownOptionClick(DELETE_OPTION)}> - delete - Delete... - + + + handleDropdownOptionClick(DELETE_OPTION)}> + delete + Delete... + + } { isQueryScreen && - handleDropdownOptionClick(CLEAR_OPTION)}> - monitor - New View - + + handleDropdownOptionClick(CLEAR_OPTION)}> + monitor + New View + + } { isQueryScreen && hasSavedReportsPermission && - handleDropdownOptionClick(NEW_REPORT_OPTION)}> - article - Create Report from Current View - + + handleDropdownOptionClick(NEW_REPORT_OPTION)}> + article + Create Report from Current View + + } { - isQueryScreen && + isQueryScreen && } Your Saved Views { - savedViews && savedViews.length > 0 ? ( - savedViews.map((record: QRecord, index: number) => + yourSavedViews && yourSavedViews.length > 0 ? ( + yourSavedViews.map((record: QRecord, index: number) => handleSavedViewRecordOnClick(record)}> {record.values.get("label")} ) - ): ( + ) : ( You do not have any saved views for this table. ) } + Views Shared with you + { + viewsSharedWithYou && viewsSharedWithYou.length > 0 ? ( + viewsSharedWithYou.map((record: QRecord, index: number) => + handleSavedViewRecordOnClick(record)}> + {record.values.get("label")} + + ) + ) : ( + + You do not have any views shared with you for this table. + + ) + }
); @@ -520,7 +566,7 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab let buttonBorder = colors.grayLines.main; let buttonColor = colors.gray.main; - if(currentSavedView) + if (currentSavedView) { if (viewIsModified) { @@ -548,23 +594,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); } @@ -593,7 +639,7 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab fontWeight: 500, fontSize: "0.875rem", p: "0.5rem", - ... buttonStyles + ...buttonStyles }} > save @@ -624,7 +670,7 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab } - + } { @@ -635,16 +681,20 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab { viewDiffs.map((s: string, i: number) =>
  • {s}
  • ) } - }> + + { + notOwnerTooltipText && {notOwnerTooltipText} + } + }> {viewDiffs.length} Unsaved Change{viewDiffs.length == 1 ? "" : "s"} - + {disabledBecauseNotOwner ? <>   : } {/* vertical rule */} - + } { @@ -663,16 +713,17 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab { viewDiffs.map((s: string, i: number) =>
  • {s}
  • ) } - }> + + }> with {viewDiffs.length} Change{viewDiffs.length == 1 ? "" : "s"} - + } {/* vertical rule */} - + }
    @@ -702,15 +753,15 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab ) : ( isSaveFilterAs ? ( Save View As - ):( + ) : ( isRenameFilter ? ( Rename View - ):( + ) : ( Update Existing View ) ) ) - ):( + ) : ( Save New View ) } @@ -721,12 +772,12 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab ) : ("")} { - (! currentSavedView || isSaveFilterAs || isRenameFilter) && ! isDeleteFilter ? ( + (!currentSavedView || isSaveFilterAs || isRenameFilter) && !isDeleteFilter ? ( { isSaveFilterAs ? ( Enter a name for this new saved view. - ):( + ) : ( Enter a new name for this saved view. ) } @@ -744,10 +795,10 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab }} /> - ):( + ) : ( isDeleteFilter ? ( Are you sure you want to delete the view {`'${currentSavedView?.values.get("label")}'`}? - ):( + ) : ( Are you sure you want to update the view {`'${currentSavedView?.values.get("label")}'`}? ) ) @@ -759,7 +810,7 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab isDeleteFilter ? : - + } diff --git a/src/qqq/components/sharing/ShareModal.tsx b/src/qqq/components/sharing/ShareModal.tsx index 57c063e..634b579 100644 --- a/src/qqq/components/sharing/ShareModal.tsx +++ b/src/qqq/components/sharing/ShareModal.tsx @@ -24,9 +24,8 @@ import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QT import {QJobComplete} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobComplete"; import {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 {Alert, Box} 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"; @@ -370,15 +369,15 @@ export default function ShareModal({open, onClose, tableMetaData, record}: Share {/* header */} - + Share {tableMetaData.label}: {record?.recordLabel ?? record?.values?.get(tableMetaData.primaryKeyField) ?? "Unknown"} - + {/* 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. + {/*You can choose if they should only be able to Read the record, or also make Edits to it.*/} - + {alert && setAlert(null)}>{alert}} {statusString} {!alert && !statusString && (<> )} @@ -390,7 +389,7 @@ export default function ShareModal({open, onClose, tableMetaData, record}: Share {/* row for adding a new share */} - + - - {renderScopeDropdown("new-share-scope", defaultScope, handleScopeChange)} - + {/* + when turning scope back on, change width of audience box to 350px + + {renderScopeDropdown("new-share-scope", defaultScope, handleScopeChange)} + + */} @@ -429,8 +431,11 @@ export default function ShareModal({open, onClose, tableMetaData, record}: Share currentShares.map((share) => ( - {share.audienceLabel} - {renderScopeDropdown(`scope-${share.shareId}`, getScopeOption(share.scopeId), (event: React.SyntheticEvent, value: any | any[], reason: string) => editingExistingShareScope(share.shareId, value))} + {share.audienceLabel} + {/* + when turning scope back on, change width of audience box to 310px + {renderScopeDropdown(`scope-${share.shareId}`, getScopeOption(share.scopeId), (event: React.SyntheticEvent, value: any | any[], reason: string) => editingExistingShareScope(share.shareId, value))} + */}