From e096e055a46f1587d5b9818960b78af0b7edc71b Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Wed, 11 Jan 2023 16:31:39 -0600 Subject: [PATCH] First pass at permissions; Updated auth0 to work with access token instead of id token --- package.json | 2 +- src/App.tsx | 77 +++++++++++++++---- src/index.tsx | 6 +- src/qqq/components/forms/EntityForm.tsx | 23 ++++-- .../processes/GoogleDriveFolderPicker.tsx | 4 +- .../components/processes/ProcessLinkCard.tsx | 27 ++++--- .../widgets/misc/RecordGridWidget.tsx | 5 +- .../widgets/statistics/MiniStatisticsCard.tsx | 5 +- src/qqq/pages/apps/Home.tsx | 62 ++++++++++++--- src/qqq/pages/apps/NoApps.tsx | 23 ++++++ src/qqq/pages/processes/ProcessRun.tsx | 32 ++++++-- src/qqq/pages/processes/ReportRun.tsx | 2 +- src/qqq/pages/records/query/RecordQuery.tsx | 23 ++++-- src/qqq/pages/records/view/RecordView.tsx | 72 +++++++++-------- src/qqq/utils/DataGridUtils.tsx | 2 +- src/qqq/utils/qqq/ValueUtils.tsx | 4 +- 16 files changed, 273 insertions(+), 96 deletions(-) create mode 100644 src/qqq/pages/apps/NoApps.tsx diff --git a/package.json b/package.json index 885fc5c..033068b 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "@auth0/auth0-react": "1.10.2", "@emotion/react": "11.7.1", "@emotion/styled": "11.6.0", - "@kingsrook/qqq-frontend-core": "1.0.40", + "@kingsrook/qqq-frontend-core": "1.0.41", "@mui/icons-material": "5.4.1", "@mui/material": "5.11.1", "@mui/styles": "5.11.1", diff --git a/src/App.tsx b/src/App.tsx index fae0804..655de15 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -40,6 +40,7 @@ import Sidenav from "qqq/components/horseshoe/sidenav/SideNav"; import theme from "qqq/components/legacy/Theme"; import {setMiniSidenav, setOpenConfigurator, useMaterialUIController} from "qqq/context"; import AppHome from "qqq/pages/apps/Home"; +import NoApps from "qqq/pages/apps/NoApps"; import ProcessRun from "qqq/pages/processes/ProcessRun"; import ReportRun from "qqq/pages/processes/ReportRun"; import EntityCreate from "qqq/pages/records/create/RecordCreate"; @@ -53,7 +54,6 @@ import ProcessUtils from "qqq/utils/qqq/ProcessUtils"; const qController = Client.getInstance(); export const SESSION_ID_COOKIE_NAME = "sessionId"; -LicenseInfo.setLicenseKey(process.env.REACT_APP_MATERIAL_UI_LICENSE_KEY); export default function App() { @@ -63,6 +63,9 @@ export default function App() const [isFullyAuthenticated, setIsFullyAuthenticated] = useState(false); const [profileRoutes, setProfileRoutes] = useState({}); const [branding, setBranding] = useState({} as QBrandingMetaData); + const [needLicenseKey, setNeedLicenseKey] = useState(true); + + const [defaultRoute, setDefaultRoute] = useState("/no-apps"); useEffect(() => { @@ -83,17 +86,21 @@ export default function App() ///////////////////////////////////////// try { - console.log("Loading token..."); - await getAccessTokenSilently(); - const idToken = await getIdTokenClaims(); - setCookie(SESSION_ID_COOKIE_NAME, idToken.__raw, {path: "/"}); + console.log("Loading token from auth0..."); + const accessToken = await getAccessTokenSilently(); + qController.setAuthorizationHeaderValue("Bearer " + accessToken); + + ///////////////////////////////////////////////////////////////////////////////// + // we've stopped using session id cook with auth0, so make sure it is not set. // + ///////////////////////////////////////////////////////////////////////////////// + removeCookie(SESSION_ID_COOKIE_NAME); + setIsFullyAuthenticated(true); console.log("Token load complete."); } catch (e) { console.log(`Error loading token: ${JSON.stringify(e)}`); - removeCookie(SESSION_ID_COOKIE_NAME); qController.clearAuthenticationMetaDataLocalStorage(); logout(); return; @@ -105,6 +112,7 @@ export default function App() // use a random token if anonymous or mock // ///////////////////////////////////////////// console.log("Generating random token..."); + qController.setAuthorizationHeaderValue(null); setIsFullyAuthenticated(true); setCookie(SESSION_ID_COOKIE_NAME, Md5.hashStr(`${new Date()}`), {path: "/"}); console.log("Token generation complete."); @@ -119,6 +127,16 @@ export default function App() })(); }, [loadingToken]); + if(needLicenseKey) + { + (async () => + { + const metaData: QInstance = await qController.loadMetaData(); + LicenseInfo.setLicenseKey(metaData.environmentValues.get("MATERIAL_UI_LICENSE_KEY") || process.env.REACT_APP_MATERIAL_UI_LICENSE_KEY); + setNeedLicenseKey(false); + })(); + } + const [controller, dispatch] = useMaterialUIController(); const {miniSidenav, direction, layout, openConfigurator, sidenavColor} = controller; const [onMouseEnter, setOnMouseEnter] = useState(false); @@ -205,6 +223,8 @@ export default function App() } } + let foundFirstApp = false; + function addAppToAppRoutesList(metaData: QInstance, app: QAppTreeNode, routeList: any[], parentPath: string, depth: number) { const path = `${parentPath}/${app.name}`; @@ -224,6 +244,16 @@ export default function App() route: path, component: , }); + + if(!foundFirstApp) + { + ///////////////////////////////////////////////////////////////////////////////////////////////////// + // keep track of what the top-most app the user has access to is. set that as their default route // + ///////////////////////////////////////////////////////////////////////////////////////////////////// + foundFirstApp = true; + setDefaultRoute(path); + console.log("Set default route to: " + path); + } } else if (app.type === QAppNodeType.TABLE) { @@ -363,14 +393,29 @@ export default function App() const sideNavAppList = [] as any[]; const appRoutesList = [] as any[]; - /////////////////////////////////////////////////////////////////////////////////// - // iterate throught the list to find the 'main dashboard so we can put it first' // - /////////////////////////////////////////////////////////////////////////////////// - for (let i = 0; i < metaData.appTree.length; i++) + ////////////////////////////////////////////////////////////////////////////////// + // iterate through the list to find the 'main dashboard so we can put it first' // + ////////////////////////////////////////////////////////////////////////////////// + if(metaData.appTree && metaData.appTree.length) { - const app = metaData.appTree[i]; - addAppToSideNavList(app, sideNavAppList, "", 0); - addAppToAppRoutesList(metaData, app, appRoutesList, "", 0); + for (let i = 0; i < metaData.appTree.length; i++) + { + const app = metaData.appTree[i]; + addAppToSideNavList(app, sideNavAppList, "", 0); + addAppToAppRoutesList(metaData, app, appRoutesList, "", 0); + } + } + else + { + /////////////////////////////////////////////////////////////////// + // if the user doesn't have access to any apps, push this route. // + /////////////////////////////////////////////////////////////////// + appRoutesList.push({ + name: "No Apps", + key: "no-apps", + route: "/no-apps", + component: , + }); } const newSideNavRoutes = []; @@ -390,10 +435,8 @@ export default function App() console.error(e); if (e instanceof QException) { - if ((e as QException).message.indexOf("status code 401") !== -1) + if ((e as QException).status === "401") { - removeCookie(SESSION_ID_COOKIE_NAME); - ////////////////////////////////////////////////////// // todo - this is auth0 logout... make more generic // ////////////////////////////////////////////////////// @@ -486,7 +529,7 @@ export default function App() onMouseLeave={handleOnMouseLeave} /> - } /> + } /> {appRoutes && getRoutes(appRoutes)} {profileRoutes && getRoutes([profileRoutes])} diff --git a/src/index.tsx b/src/index.tsx index db91fa4..a5b5f71 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -81,6 +81,9 @@ authenticationMetaDataPromise.then((authenticationMetaData) => // @ts-ignore const clientId = authenticationMetaData.data.clientId; + // @ts-ignore + const audience = authenticationMetaData.data.audience; + if(!domain || !clientId) { render( @@ -103,7 +106,8 @@ authenticationMetaDataPromise.then((authenticationMetaData) => diff --git a/src/qqq/components/forms/EntityForm.tsx b/src/qqq/components/forms/EntityForm.tsx index 78f68b0..287795f 100644 --- a/src/qqq/components/forms/EntityForm.tsx +++ b/src/qqq/components/forms/EntityForm.tsx @@ -88,7 +88,7 @@ function EntityForm(props: Props): JSX.Element const [tableSections, setTableSections] = useState(null as QTableSection[]); const [, forceUpdate] = useReducer((x) => x + 1, 0); - const [noCapabilityError, setNoCapabilityError] = useState(null as string); + const [notAllowedError, setNotAllowedError] = useState(null as string); const {pageHeader, setPageHeader} = useContext(QContext); @@ -189,7 +189,11 @@ function EntityForm(props: Props): JSX.Element if (!tableMetaData.capabilities.has(Capability.TABLE_UPDATE)) { - setNoCapabilityError("You may not edit records in this table"); + setNotAllowedError("Records may not be edited in this table"); + } + else if (!tableMetaData.editPermission) + { + setNotAllowedError(`You do not have permission to edit ${tableMetaData.label} records`); } } else @@ -206,7 +210,11 @@ function EntityForm(props: Props): JSX.Element if (!tableMetaData.capabilities.has(Capability.TABLE_INSERT)) { - setNoCapabilityError("You may not create records in this table"); + setNotAllowedError("Records may not be created in this table"); + } + else if (!tableMetaData.insertPermission) + { + setNotAllowedError(`You do not have permission to create ${tableMetaData.label} records`); } //////////////////////////////////////////////////////////////////////////////////////////////// @@ -420,14 +428,19 @@ function EntityForm(props: Props): JSX.Element const formId = props.id != null ? `edit-${tableMetaData?.name}-form` : `create-${tableMetaData?.name}-form`; let body; - if (noCapabilityError) + if (notAllowedError) { body = ( - {noCapabilityError} + {notAllowedError} + {props.isModal && + + + + } diff --git a/src/qqq/components/processes/GoogleDriveFolderPicker.tsx b/src/qqq/components/processes/GoogleDriveFolderPicker.tsx index 22d5a78..3c2899d 100644 --- a/src/qqq/components/processes/GoogleDriveFolderPicker.tsx +++ b/src/qqq/components/processes/GoogleDriveFolderPicker.tsx @@ -39,8 +39,8 @@ interface Props export function GoogleDriveFolderPicker({showDefaultFoldersView, showSharedDrivesView, qInstance}: Props): JSX.Element { - const clientId = "649816208522-m6oa971vqicrc1hlam7333pt4qck0tm8.apps.googleusercontent.com"; - const appApiKey = "AIzaSyBhXK34CF2fUfCgUS1VIHoKZbHxEBuHtDM"; + const clientId = qInstance.environmentValues.get("GOOGLE_APP_CLIENT_ID") || process.env.REACT_APP_GOOGLE_APP_CLIENT_ID; + const appApiKey = qInstance.environmentValues.get("GOOGLE_APP_API_KEY") || process.env.REACT_APP_GOOGLE_APP_API_KEY; if(!clientId) { console.error("Missing environmentValue GOOGLE_APP_CLIENT_ID") diff --git a/src/qqq/components/processes/ProcessLinkCard.tsx b/src/qqq/components/processes/ProcessLinkCard.tsx index 958b605..a86e786 100644 --- a/src/qqq/components/processes/ProcessLinkCard.tsx +++ b/src/qqq/components/processes/ProcessLinkCard.tsx @@ -24,6 +24,7 @@ import Card from "@mui/material/Card"; import Divider from "@mui/material/Divider"; import Icon from "@mui/material/Icon"; import {ReactNode} from "react"; +import colors from "qqq/assets/theme/base/colors"; import MDTypography from "qqq/components/legacy/MDTypography"; interface Props @@ -37,33 +38,34 @@ interface Props label: string; }; icon: ReactNode; + isDisabled?: boolean; [key: string]: any; } function ProcessLinkCard({ - color, isReport, title, percentage, icon, + color, isReport, title, percentage, icon, isDisabled }: Props): JSX.Element { return ( - + {icon} - + {title} @@ -81,9 +83,15 @@ function ProcessLinkCard({ {percentage.amount} { - isReport - ? `Click here to access the ${title} report.` - : `Click here to run the process called ${title}.` + isDisabled ? ( + isReport + ? `You do not have permission to access the ${title} report.` + : `You do not have permission to run the process called ${title}.` + ) : ( + isReport + ? `Click here to access the ${title} report.` + : `Click here to run the process called ${title}.` + ) } {percentage.label} @@ -100,6 +108,7 @@ ProcessLinkCard.defaultProps = { text: "", label: "", }, + isDisabled: false, }; export default ProcessLinkCard; diff --git a/src/qqq/components/widgets/misc/RecordGridWidget.tsx b/src/qqq/components/widgets/misc/RecordGridWidget.tsx index 954ef1f..c076491 100644 --- a/src/qqq/components/widgets/misc/RecordGridWidget.tsx +++ b/src/qqq/components/widgets/misc/RecordGridWidget.tsx @@ -61,7 +61,10 @@ function RecordGridWidget({title, data}: Props): JSX.Element const tableMetaData = new QTableMetaData(data.childTableMetaData); const {rows, columnsToRender} = DataGridUtils.makeRows(records, tableMetaData); - const childTablePath = data.tablePath + (data.tablePath.endsWith("/") ? "" : "/") + ///////////////////////////////////////////////////////////////////////////////// + // note - tablePath may be null, if the user doesn't have access to the table. // + ///////////////////////////////////////////////////////////////////////////////// + const childTablePath = data.tablePath ? data.tablePath + (data.tablePath.endsWith("/") ? "" : "/") : data.tablePath; const columns = DataGridUtils.setupGridColumns(tableMetaData, columnsToRender, childTablePath); //////////////////////////////////////////////////////////////// diff --git a/src/qqq/components/widgets/statistics/MiniStatisticsCard.tsx b/src/qqq/components/widgets/statistics/MiniStatisticsCard.tsx index 5d933f1..e880bf1 100644 --- a/src/qqq/components/widgets/statistics/MiniStatisticsCard.tsx +++ b/src/qqq/components/widgets/statistics/MiniStatisticsCard.tsx @@ -46,6 +46,7 @@ interface Props { component: ReactNode; }; direction?: "right" | "left"; + isDisabled?: boolean; [key: string]: any; } @@ -56,6 +57,7 @@ function MiniStatisticsCard({ percentage, icon, direction, + isDisabled, }: Props): JSX.Element { const [controller] = useMaterialUIController(); @@ -108,7 +110,7 @@ function MiniStatisticsCard({ justifyContent="center" alignItems="center" color="#FFFFFF" - sx={{borderRadius: "10px", backgroundColor: colors.info.main}} + sx={{borderRadius: "10px", backgroundColor: isDisabled ? colors.secondary.main : colors.info.main}} > {icon.component} @@ -134,6 +136,7 @@ MiniStatisticsCard.defaultProps = { text: "", }, direction: "right", + isDisabled: false, }; export default MiniStatisticsCard; diff --git a/src/qqq/pages/apps/Home.tsx b/src/qqq/pages/apps/Home.tsx index 00aceab..223ad79 100644 --- a/src/qqq/pages/apps/Home.tsx +++ b/src/qqq/pages/apps/Home.tsx @@ -125,7 +125,7 @@ function AppHome({app}: Props): JSX.Element { const tableMetaData = await qController.loadTableMetaData(table.name); let countResult = null; - if(tableMetaData.capabilities.has(Capability.TABLE_COUNT)) + if(tableMetaData.capabilities.has(Capability.TABLE_COUNT) && tableMetaData.readPermission) { countResult = await qController.count(table.name); @@ -183,6 +183,20 @@ function AppHome({app}: Props): JSX.Element }, 1); }; + const hasTablePermission = (tableName: string) => + { + return tables.find(t => t.name === tableName && (t.readPermission || t.insertPermission || t.editPermission || t.deletePermission)); + }; + + const hasProcessPermission = (processName: string) => + { + return processes.find(p => p.name === processName && p.hasPermission); + }; + + const hasReportPermission = (reportName: string) => + { + return reports.find(r => r.name === reportName && r.hasPermission); + }; return ( @@ -210,7 +224,7 @@ function AppHome({app}: Props): JSX.Element { section.processes ? ( - + Actions ) : null @@ -224,12 +238,19 @@ function AppHome({app}: Props): JSX.Element let process = app.childMap.get(processName); return ( - + {hasProcessPermission(processName) ? + + + : - + } ); }) @@ -244,7 +265,7 @@ function AppHome({app}: Props): JSX.Element } { section.reports ? ( - + Reports ) : null @@ -258,13 +279,21 @@ function AppHome({app}: Props): JSX.Element let report = app.childMap.get(reportName); return ( - + {hasReportPermission(reportName) ? + + + : - + } ); }) @@ -279,7 +308,7 @@ function AppHome({app}: Props): JSX.Element } { section.tables ? ( - + Data ) : null @@ -293,16 +322,27 @@ function AppHome({app}: Props): JSX.Element let table = app.childMap.get(tableName); return ( - - + {hasTablePermission(tableName) ? + + + {table.iconName || app.iconName}}} + /> + + : + {table.iconName || app.iconName}}} + isDisabled={true} /> - + } ); }) diff --git a/src/qqq/pages/apps/NoApps.tsx b/src/qqq/pages/apps/NoApps.tsx new file mode 100644 index 0000000..e5da2e7 --- /dev/null +++ b/src/qqq/pages/apps/NoApps.tsx @@ -0,0 +1,23 @@ +import {Alert} from "@mui/material"; +import BaseLayout from "qqq/layouts/BaseLayout"; + + +interface Props +{ + foo: string; +} + +NoApps.defaultProps = { + foo: null, +}; + +function NoApps({foo}: Props): JSX.Element +{ + return ( + + You do not have permission to access any apps. + + ); +} + +export default NoApps; diff --git a/src/qqq/pages/processes/ProcessRun.tsx b/src/qqq/pages/processes/ProcessRun.tsx index 1ef7e38..2ddd9f0 100644 --- a/src/qqq/pages/processes/ProcessRun.tsx +++ b/src/qqq/pages/processes/ProcessRun.tsx @@ -71,16 +71,18 @@ interface Props defaultProcessValues?: any; isModal?: boolean; isWidget?: boolean; + isReport?: boolean; recordIds?: string | QQueryFilter; closeModalHandler?: (event: object, reason: string) => void; forceReInit?: number; + overrideLabel?: string } const INITIAL_RETRY_MILLIS = 1_500; const RETRY_MAX_MILLIS = 12_000; const BACKOFF_AMOUNT = 1.5; -function ProcessRun({process, defaultProcessValues, isModal, isWidget, recordIds, closeModalHandler, forceReInit}: Props): JSX.Element +function ProcessRun({process, defaultProcessValues, isModal, isWidget, isReport, recordIds, closeModalHandler, forceReInit, overrideLabel}: Props): JSX.Element { const processNameParam = useParams().processName; const processName = process === null ? processNameParam : process.name; @@ -236,9 +238,9 @@ function ProcessRun({process, defaultProcessValues, isModal, isWidget, recordIds Error - An error occurred while running the process: + An error occurred while running the {isReport ? "report" : "process"}: {" "} - {process.label} + {overrideLabel ?? process.label} { isUserFacingError ? ( @@ -331,7 +333,7 @@ function ProcessRun({process, defaultProcessValues, isModal, isWidget, recordIds /////////////////////////////////////////////////////////////////////////////////////////////////////////////// !isWidget && - {(isModal) ? `${process.label}: ` : ""} + {(isModal) ? `${overrideLabel ?? process.label}: ` : ""} {step?.label} } @@ -616,7 +618,7 @@ function ProcessRun({process, defaultProcessValues, isModal, isWidget, recordIds if(! isWidget) { - setPageHeader(processMetaData.label); + setPageHeader(overrideLabel ?? processMetaData.label); } let newIndex = null; @@ -935,6 +937,18 @@ function ProcessRun({process, defaultProcessValues, isModal, isWidget, recordIds } }, [needToCheckJobStatus, retryMillis]); + + const handlePermissionDenied = (e: any): boolean => + { + if ((e as QException).status === "403") + { + setProcessError(`You do not have permission to run this ${isReport ? "report" : "process"}.`, true) + return (true); + } + return (false); + } + + ////////////////////////////////////////////////////////////////////////////////////////// // do the initial load of data for the process - that is, meta data, plus the init step // // also - allow the component that contains this component to force a re-init, by // @@ -1011,7 +1025,7 @@ function ProcessRun({process, defaultProcessValues, isModal, isWidget, recordIds } catch (e) { - setProcessError("Error loading process definition."); + handlePermissionDenied(e) || setProcessError("Error loading process definition."); return; } @@ -1031,7 +1045,7 @@ function ProcessRun({process, defaultProcessValues, isModal, isWidget, recordIds } catch (e) { - setProcessError("Error initializing process."); + handlePermissionDenied(e) || setProcessError("Error initializing process."); } })(); } @@ -1286,9 +1300,11 @@ ProcessRun.defaultProps = { defaultProcessValues: {}, isModal: false, isWidget: false, + isReport: false, recordIds: null, closeModalHandler: null, - forceReInit: 0 + forceReInit: 0, + overrideLabel: null, }; export default ProcessRun; diff --git a/src/qqq/pages/processes/ReportRun.tsx b/src/qqq/pages/processes/ReportRun.tsx index 872d2cf..04186e6 100644 --- a/src/qqq/pages/processes/ReportRun.tsx +++ b/src/qqq/pages/processes/ReportRun.tsx @@ -53,7 +53,7 @@ function ReportRun({report}: Props): JSX.Element setPageHeader(report.label); const process = metaData.processes.get(report.processName); const defaultProcessValues = {reportName: report.name}; - return (); + return (); } else { diff --git a/src/qqq/pages/records/query/RecordQuery.tsx b/src/qqq/pages/records/query/RecordQuery.tsx index 2262383..2538be2 100644 --- a/src/qqq/pages/records/query/RecordQuery.tsx +++ b/src/qqq/pages/records/query/RecordQuery.tsx @@ -1044,27 +1044,27 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element keepMounted > { - table.capabilities.has(Capability.TABLE_INSERT) && + table.capabilities.has(Capability.TABLE_INSERT) && table.insertPermission && library_add Bulk Load } { - table.capabilities.has(Capability.TABLE_UPDATE) && + table.capabilities.has(Capability.TABLE_UPDATE) && table.editPermission && edit Bulk Edit } { - table.capabilities.has(Capability.TABLE_DELETE) && + table.capabilities.has(Capability.TABLE_DELETE) && table.deletePermission && delete Bulk Delete } - {(table.capabilities.has(Capability.TABLE_INSERT) || table.capabilities.has(Capability.TABLE_UPDATE) || table.capabilities.has(Capability.TABLE_DELETE)) && tableProcesses.length > 0 && } + {((table.capabilities.has(Capability.TABLE_INSERT) && table.insertPermission) || (table.capabilities.has(Capability.TABLE_UPDATE) && table.editPermission) || (table.capabilities.has(Capability.TABLE_DELETE) && table.deletePermission)) && tableProcesses.length > 0 && } {tableProcesses.map((process) => ( processClicked(process)}> {process.iconName ?? "arrow_forward"} @@ -1072,7 +1072,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element ))} { - tableProcesses.length == 0 && !table.capabilities.has(Capability.TABLE_INSERT) && !table.capabilities.has(Capability.TABLE_UPDATE) && !table.capabilities.has(Capability.TABLE_DELETE) && + tableProcesses.length == 0 && !(table.capabilities.has(Capability.TABLE_INSERT) && table.insertPermission) && !(table.capabilities.has(Capability.TABLE_UPDATE) && table.editPermission) && !(table.capabilities.has(Capability.TABLE_DELETE) && table.deletePermission) && block No actions available @@ -1112,6 +1112,17 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element document.scrollingElement.scrollTop = 0; }, [ pageNumber, rowsPerPage ]); + if(tableMetaData && !tableMetaData.readPermission) + { + return ( + + + + You do not have permission to view {tableMetaData?.label} records + + + ); + } return ( @@ -1148,7 +1159,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element { - table.capabilities.has(Capability.TABLE_INSERT) && + table.capabilities.has(Capability.TABLE_INSERT) && table.insertPermission && } diff --git a/src/qqq/pages/records/view/RecordView.tsx b/src/qqq/pages/records/view/RecordView.tsx index cec6ada..b9a49c3 100644 --- a/src/qqq/pages/records/view/RecordView.tsx +++ b/src/qqq/pages/records/view/RecordView.tsx @@ -95,6 +95,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element const [allTableProcesses, setAllTableProcesses] = useState([] as QProcessMetaData[]); const [actionsMenu, setActionsMenu] = useState(null); const [notFoundMessage, setNotFoundMessage] = useState(null); + const [successMessage, setSuccessMessage] = useState(null as string); const [searchParams] = useSearchParams(); const {setPageHeader} = useContext(QContext); const [activeModalProcess, setActiveModalProcess] = useState(null as QProcessMetaData); @@ -108,6 +109,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element const reload = () => { + setSuccessMessage(null); setNotFoundMessage(null); setAsyncLoadInited(false); setTableMetaData(null); @@ -267,21 +269,30 @@ function RecordView({table, launchProcess}: Props): JSX.Element } catch (e) { + const historyPurge = (path: string) => + { + try + { + HistoryUtils.ensurePathNotInHistory(location.pathname); + } + catch(e) + { + console.error("Error pushing history: " + e); + } + } + if (e instanceof QException) { if ((e as QException).status === "404") { setNotFoundMessage(`${tableMetaData.label} ${id} could not be found.`); - - try - { - HistoryUtils.ensurePathNotInHistory(location.pathname); - } - catch(e) - { - console.error("Error pushing history: " + e); - } - + historyPurge(location.pathname) + return; + } + else if ((e as QException).status === "403") + { + setNotFoundMessage(`You do not have permission to view ${tableMetaData.label} records`); + historyPurge(location.pathname) return; } } @@ -298,7 +309,6 @@ function RecordView({table, launchProcess}: Props): JSX.Element console.error("Error pushing history: " + e); } - ///////////////////////////////////////////////// // define the sections, e.g., for the left-bar // ///////////////////////////////////////////////// @@ -344,10 +354,10 @@ function RecordView({table, launchProcess}: Props): JSX.Element { section.fieldNames.map((fieldName: string) => ( - + {tableMetaData.fields.get(fieldName).label}: - + {ValueUtils.getDisplayValue(tableMetaData.fields.get(fieldName), record, "view")} @@ -396,6 +406,11 @@ function RecordView({table, launchProcess}: Props): JSX.Element setSectionFieldElements(sectionFieldElements); setNonT1TableSections(nonT1TableSections); + if (searchParams.get("createSuccess") || searchParams.get("updateSuccess")) + { + setSuccessMessage(`${tableMetaData.label} successfully ${searchParams.get("createSuccess") ? "created" : "updated"}`); + } + forceUpdate(); })(); } @@ -445,14 +460,14 @@ function RecordView({table, launchProcess}: Props): JSX.Element keepMounted > { - table.capabilities.has(Capability.TABLE_UPDATE) && + table.capabilities.has(Capability.TABLE_UPDATE) && table.editPermission && navigate("edit")}> edit Edit } { - table.capabilities.has(Capability.TABLE_DELETE) && + table.capabilities.has(Capability.TABLE_DELETE) && table.deletePermission && { setActionsMenu(null); @@ -463,7 +478,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element Delete } - {tableProcesses.length > 0 && (table.capabilities.has(Capability.TABLE_UPDATE) || table.capabilities.has(Capability.TABLE_DELETE)) && } + {tableProcesses.length > 0 && ((table.capabilities.has(Capability.TABLE_UPDATE) && table.editPermission) || (table.capabilities.has(Capability.TABLE_DELETE) && table.deletePermission)) && } {tableProcesses.map((process) => ( processClicked(process)}> {process.iconName ?? "arrow_forward"} @@ -552,21 +567,18 @@ function RecordView({table, launchProcess}: Props): JSX.Element { notFoundMessage ? - {notFoundMessage} + {notFoundMessage} : { - (searchParams.get("createSuccess") || searchParams.get("updateSuccess")) ? ( - - {}}> - {tableMetaData?.label} - {" "} - successfully - {" "} - {searchParams.get("createSuccess") ? "created" : "updated"} - + successMessage ? + + { + setSuccessMessage(null) + }}> + {successMessage} - ) : ("") + : ("") } @@ -588,7 +600,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element - {tableMetaData && record ? `Viewing ${tableMetaData?.label}: ${record?.recordLabel}` : ""} + {tableMetaData && record ? `Viewing ${tableMetaData?.label}: ${record?.recordLabel || ""}` : ""} {renderActionsMenu} @@ -610,10 +622,10 @@ function RecordView({table, launchProcess}: Props): JSX.Element { - table.capabilities.has(Capability.TABLE_DELETE) && + table.capabilities.has(Capability.TABLE_DELETE) && table.deletePermission && } { - table.capabilities.has(Capability.TABLE_UPDATE) && + table.capabilities.has(Capability.TABLE_UPDATE) && table.editPermission && } diff --git a/src/qqq/utils/DataGridUtils.tsx b/src/qqq/utils/DataGridUtils.tsx index 3241ac9..0438872 100644 --- a/src/qqq/utils/DataGridUtils.tsx +++ b/src/qqq/utils/DataGridUtils.tsx @@ -182,7 +182,7 @@ export default class DataGridUtils ); } - if (key === tableMetaData.primaryKeyField) + if (key === tableMetaData.primaryKeyField && linkBase) { columns.splice(0, 0, column); column.renderCell = (cellValues: any) => ( diff --git a/src/qqq/utils/qqq/ValueUtils.tsx b/src/qqq/utils/qqq/ValueUtils.tsx index 3f45a21..4ee1017 100644 --- a/src/qqq/utils/qqq/ValueUtils.tsx +++ b/src/qqq/utils/qqq/ValueUtils.tsx @@ -95,8 +95,8 @@ class ValueUtils let tablePath = ValueUtils.getQInstance().getTablePathByName(toRecordFromTable); if (!tablePath) { - console.log("Couldn't find path for table: " + tablePath); - return (""); + console.log("Couldn't find path for table: " + toRecordFromTable); + return (displayValue ?? rawValue); } if (!tablePath.endsWith("/"))