mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-21 14:48:43 +00:00
Compare commits
2 Commits
main
...
snapshot-i
Author | SHA1 | Date | |
---|---|---|---|
ce9ffaab4d | |||
9816403bec |
1
.gitignore
vendored
1
.gitignore
vendored
@ -5,7 +5,6 @@
|
||||
.yalc*
|
||||
yalc.lock
|
||||
.env
|
||||
/certs
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
@ -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.119",
|
||||
"@kingsrook/qqq-frontend-core": "1.0.114",
|
||||
"@mui/icons-material": "5.4.1",
|
||||
"@mui/material": "5.11.1",
|
||||
"@mui/styles": "5.11.1",
|
||||
@ -36,8 +36,6 @@
|
||||
"html-to-text": "^9.0.5",
|
||||
"http-proxy-middleware": "2.0.6",
|
||||
"jwt-decode": "3.1.2",
|
||||
"oidc-client-ts": "2.4.1",
|
||||
"react-oidc-context": "2.3.1",
|
||||
"rapidoc": "9.3.4",
|
||||
"react": "18.0.0",
|
||||
"react-ace": "10.1.0",
|
||||
|
4
pom.xml
4
pom.xml
@ -29,7 +29,7 @@
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<properties>
|
||||
<revision>0.25.0</revision>
|
||||
<revision>0.24.0-SNAPSHOT</revision>
|
||||
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
@ -66,7 +66,7 @@
|
||||
<dependency>
|
||||
<groupId>com.kingsrook.qqq</groupId>
|
||||
<artifactId>qqq-backend-core</artifactId>
|
||||
<version>0.25.0-integration-sprint-62-20250307-205536</version>
|
||||
<version>0.21.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
|
234
src/App.tsx
234
src/App.tsx
@ -19,6 +19,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {useAuth0} from "@auth0/auth0-react";
|
||||
import {QException} from "@kingsrook/qqq-frontend-core/lib/exceptions/QException";
|
||||
import {QAppNodeType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAppNodeType";
|
||||
import {QAppTreeNode} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAppTreeNode";
|
||||
@ -28,20 +29,16 @@ import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstan
|
||||
import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData";
|
||||
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
||||
import Avatar from "@mui/material/Avatar";
|
||||
import Box from "@mui/material/Box";
|
||||
import CssBaseline from "@mui/material/CssBaseline";
|
||||
import Icon from "@mui/material/Icon";
|
||||
import {ThemeProvider} from "@mui/material/styles";
|
||||
import {LicenseInfo} from "@mui/x-license-pro";
|
||||
import CommandMenu from "CommandMenu";
|
||||
import jwt_decode from "jwt-decode";
|
||||
import QContext from "QContext";
|
||||
import useAnonymousAuthenticationModule from "qqq/authorization/anonymous/useAnonymousAuthenticationModule";
|
||||
import useAuth0AuthenticationModule from "qqq/authorization/auth0/useAuth0AuthenticationModule";
|
||||
import useOAuth2AuthenticationModule from "qqq/authorization/oauth2/useOAuth2AuthenticationModule";
|
||||
import Sidenav from "qqq/components/horseshoe/sidenav/SideNav";
|
||||
import theme from "qqq/components/legacy/Theme";
|
||||
import {getBannerClassName, getBannerStyles, getBanner, makeBannerContent} from "qqq/components/misc/Banners";
|
||||
import {setMiniSidenav, useMaterialUIController} from "qqq/context";
|
||||
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";
|
||||
@ -65,14 +62,10 @@ import {Md5} from "ts-md5/dist/md5";
|
||||
const qController = Client.getInstance();
|
||||
export const SESSION_UUID_COOKIE_NAME = "sessionUUID";
|
||||
|
||||
interface Props
|
||||
export default function App()
|
||||
{
|
||||
authenticationMetaData: QAuthenticationMetaData;
|
||||
}
|
||||
|
||||
export default function App({authenticationMetaData}: Props)
|
||||
{
|
||||
const [, , removeCookie] = useCookies([SESSION_UUID_COOKIE_NAME]);
|
||||
const [cookies, setCookie, removeCookie] = useCookies([SESSION_UUID_COOKIE_NAME]);
|
||||
const {user, getAccessTokenSilently, logout} = useAuth0();
|
||||
const [loadingToken, setLoadingToken] = useState(false);
|
||||
const [isFullyAuthenticated, setIsFullyAuthenticated] = useState(false);
|
||||
const [profileRoutes, setProfileRoutes] = useState({});
|
||||
@ -81,20 +74,68 @@ export default function App({authenticationMetaData}: Props)
|
||||
const [needLicenseKey, setNeedLicenseKey] = useState(true);
|
||||
const [loggedInUser, setLoggedInUser] = useState({} as { name?: string, email?: string });
|
||||
const [defaultRoute, setDefaultRoute] = useState("/no-apps");
|
||||
const [earlyReturnForAuth, setEarlyReturnForAuth] = useState(null as JSX.Element);
|
||||
|
||||
const {setupSession: auth0SetupSession, logout: auth0Logout} = useAuth0AuthenticationModule({setIsFullyAuthenticated, setLoggedInUser, setEarlyReturnForAuth});
|
||||
const {setupSession: oauth2SetupSession, logout: oauth2Logout} = useOAuth2AuthenticationModule({setIsFullyAuthenticated, setLoggedInUser, setEarlyReturnForAuth, inOAuthContext: authenticationMetaData.type === "OAUTH2"});
|
||||
const {setupSession: anonymousSetupSession, logout: anonymousLogout} = useAnonymousAuthenticationModule({setIsFullyAuthenticated, setLoggedInUser, setEarlyReturnForAuth});
|
||||
|
||||
/////////////////////////////////////////////////////////
|
||||
// tell the client how to do a logout if it sees a 401 //
|
||||
/////////////////////////////////////////////////////////
|
||||
Client.setUnauthorizedCallback(() => doLogout());
|
||||
Client.setUnauthorizedCallback(() =>
|
||||
{
|
||||
logout();
|
||||
});
|
||||
|
||||
const shouldStoreNewToken = (newToken: string, oldToken: string): boolean =>
|
||||
{
|
||||
if (!cookies[SESSION_UUID_COOKIE_NAME])
|
||||
{
|
||||
console.log("No session uuid cookie - so we should store a new one.");
|
||||
return (true);
|
||||
}
|
||||
|
||||
if (!oldToken)
|
||||
{
|
||||
console.log("No accessToken in localStorage - so we should store a new one.");
|
||||
return (true);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
const oldJSON: any = jwt_decode(oldToken);
|
||||
const newJSON: any = jwt_decode(newToken);
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
// if the old (local storage) token is expired, then we need to store the new one //
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
const oldExp = oldJSON["exp"];
|
||||
if (oldExp * 1000 < (new Date().getTime()))
|
||||
{
|
||||
console.log("Access token in local storage was expired - so we should store a new one.");
|
||||
return (true);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// remove the exp & iat values from what we compare - as they are always different from auth0 //
|
||||
// note, this is only deleting them from what we compare, not from what we'd store. //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
delete newJSON["exp"];
|
||||
delete newJSON["iat"];
|
||||
delete oldJSON["exp"];
|
||||
delete oldJSON["iat"];
|
||||
|
||||
const different = JSON.stringify(newJSON) !== JSON.stringify(oldJSON);
|
||||
if (different)
|
||||
{
|
||||
console.log("Latest access token from auth0 has changed vs localStorage - so we should store a new one.");
|
||||
}
|
||||
return (different);
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
console.log("Caught in shouldStoreNewToken: " + e);
|
||||
}
|
||||
|
||||
return (true);
|
||||
};
|
||||
|
||||
/////////////////////////////////////////////////
|
||||
// deal with making sure user is authenticated //
|
||||
/////////////////////////////////////////////////
|
||||
useEffect(() =>
|
||||
{
|
||||
if (loadingToken)
|
||||
@ -105,17 +146,65 @@ export default function App({authenticationMetaData}: Props)
|
||||
|
||||
(async () =>
|
||||
{
|
||||
const authenticationMetaData: QAuthenticationMetaData = await qController.getAuthenticationMetaData();
|
||||
|
||||
if (authenticationMetaData.type === "AUTH_0")
|
||||
{
|
||||
await auth0SetupSession();
|
||||
}
|
||||
else if (authenticationMetaData.type === "OAUTH2")
|
||||
{
|
||||
await oauth2SetupSession();
|
||||
/////////////////////////////////////////
|
||||
// use auth0 if auth type is ... auth0 //
|
||||
/////////////////////////////////////////
|
||||
try
|
||||
{
|
||||
console.log("Loading token from auth0...");
|
||||
const accessToken = await getAccessTokenSilently();
|
||||
|
||||
const lsAccessToken = localStorage.getItem("accessToken");
|
||||
if (shouldStoreNewToken(accessToken, lsAccessToken))
|
||||
{
|
||||
console.log("Sending accessToken to backend, requesting a sessionUUID...");
|
||||
const {uuid: newSessionUuid, values} = await qController.manageSession(accessToken, null);
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// the request to the backend should send a header to set the cookie, so we don't need to do it ourselves. //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// setCookie(SESSION_UUID_COOKIE_NAME, newSessionUuid, {path: "/"});
|
||||
|
||||
localStorage.setItem("accessToken", accessToken);
|
||||
localStorage.setItem("sessionValues", JSON.stringify(values));
|
||||
console.log("Got new sessionUUID from backend, and stored new accessToken");
|
||||
}
|
||||
else
|
||||
{
|
||||
console.log("Using existing sessionUUID cookie");
|
||||
}
|
||||
|
||||
setIsFullyAuthenticated(true);
|
||||
qController.setGotAuthentication();
|
||||
|
||||
setLoggedInUser(user);
|
||||
console.log("Token load complete.");
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
console.log(`Error loading token: ${JSON.stringify(e)}`);
|
||||
qController.clearAuthenticationMetaDataLocalStorage();
|
||||
localStorage.removeItem("accessToken");
|
||||
removeCookie(SESSION_UUID_COOKIE_NAME, {path: "/"});
|
||||
logout();
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (authenticationMetaData.type === "FULLY_ANONYMOUS" || authenticationMetaData.type === "MOCK")
|
||||
{
|
||||
await anonymousSetupSession();
|
||||
/////////////////////////////////////////////
|
||||
// use a random token if anonymous or mock //
|
||||
/////////////////////////////////////////////
|
||||
console.log("Generating random token...");
|
||||
setIsFullyAuthenticated(true);
|
||||
qController.setGotAuthentication();
|
||||
setCookie(SESSION_UUID_COOKIE_NAME, Md5.hashStr(`${new Date()}`), {path: "/"});
|
||||
console.log("Token generation complete.");
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -131,36 +220,13 @@ export default function App({authenticationMetaData}: Props)
|
||||
(async () =>
|
||||
{
|
||||
const metaData: QInstance = await qController.loadMetaData();
|
||||
LicenseInfo.setLicenseKey(metaData.environmentValues?.get("MATERIAL_UI_LICENSE_KEY") || process.env.REACT_APP_MATERIAL_UI_LICENSE_KEY);
|
||||
LicenseInfo.setLicenseKey(metaData.environmentValues.get("MATERIAL_UI_LICENSE_KEY") || process.env.REACT_APP_MATERIAL_UI_LICENSE_KEY);
|
||||
setNeedLicenseKey(false);
|
||||
})();
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
** call appropriate logout function based on authentication meta data type
|
||||
***************************************************************************/
|
||||
function doLogout()
|
||||
{
|
||||
if (authenticationMetaData?.type === "AUTH_0")
|
||||
{
|
||||
auth0Logout();
|
||||
}
|
||||
else if (authenticationMetaData?.type === "OAUTH2")
|
||||
{
|
||||
oauth2Logout();
|
||||
}
|
||||
else if (authenticationMetaData?.type === "FULLY_ANONYMOUS" || authenticationMetaData?.type === "MOCK")
|
||||
{
|
||||
anonymousLogout();
|
||||
}
|
||||
else
|
||||
{
|
||||
console.log(`No logout callback for authentication type [${authenticationMetaData?.type}].`);
|
||||
}
|
||||
}
|
||||
|
||||
const [controller, dispatch] = useMaterialUIController();
|
||||
const {miniSidenav, direction, sidenavColor} = controller;
|
||||
const {miniSidenav, direction, layout, openConfigurator, sidenavColor} = controller;
|
||||
const [onMouseEnter, setOnMouseEnter] = useState(false);
|
||||
const {pathname} = useLocation();
|
||||
const [queryParams] = useSearchParams();
|
||||
@ -453,10 +519,11 @@ export default function App({authenticationMetaData}: Props)
|
||||
}
|
||||
}
|
||||
|
||||
let profileRoutes = {};
|
||||
const gravatarBase = "https://www.gravatar.com/avatar/";
|
||||
const hash = Md5.hashStr(loggedInUser?.email || "user");
|
||||
const profilePicture = `${gravatarBase}${hash}`;
|
||||
const profileRoutes = {
|
||||
profileRoutes = {
|
||||
type: "collapse",
|
||||
name: loggedInUser?.name ?? "Anonymous",
|
||||
key: "username",
|
||||
@ -525,7 +592,10 @@ export default function App({authenticationMetaData}: Props)
|
||||
localStorage.removeItem("accessToken");
|
||||
removeCookie(SESSION_UUID_COOKIE_NAME, {path: "/"});
|
||||
|
||||
doLogout();
|
||||
//////////////////////////////////////////////////////
|
||||
// todo - this is auth0 logout... make more generic //
|
||||
//////////////////////////////////////////////////////
|
||||
logout();
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -533,9 +603,7 @@ export default function App({authenticationMetaData}: Props)
|
||||
})();
|
||||
}, [needToLoadRoutes, isFullyAuthenticated]);
|
||||
|
||||
///////////////////////////////////////////////////
|
||||
// Open sidenav when mouse enter on mini sidenav //
|
||||
///////////////////////////////////////////////////
|
||||
// Open sidenav when mouse enter on mini sidenav
|
||||
const handleOnMouseEnter = () =>
|
||||
{
|
||||
if (miniSidenav && !onMouseEnter)
|
||||
@ -545,9 +613,7 @@ export default function App({authenticationMetaData}: Props)
|
||||
}
|
||||
};
|
||||
|
||||
/////////////////////////////////////////////////
|
||||
// Close sidenav when mouse leave mini sidenav //
|
||||
/////////////////////////////////////////////////
|
||||
// Close sidenav when mouse leave mini sidenav
|
||||
const handleOnMouseLeave = () =>
|
||||
{
|
||||
if (onMouseEnter)
|
||||
@ -557,14 +623,16 @@ export default function App({authenticationMetaData}: Props)
|
||||
}
|
||||
};
|
||||
|
||||
// Change the openConfigurator state
|
||||
const handleConfiguratorOpen = () => setOpenConfigurator(dispatch, !openConfigurator);
|
||||
|
||||
// Setting the dir attribute for the body element
|
||||
useEffect(() =>
|
||||
{
|
||||
document.body.setAttribute("dir", direction);
|
||||
}, [direction]);
|
||||
|
||||
//////////////////////////////////////////////////////
|
||||
// Setting page scroll to 0 when changing the route //
|
||||
//////////////////////////////////////////////////////
|
||||
// Setting page scroll to 0 when changing the route
|
||||
useEffect(() =>
|
||||
{
|
||||
document.documentElement.scrollTop = 0;
|
||||
@ -604,14 +672,14 @@ export default function App({authenticationMetaData}: Props)
|
||||
const [dotMenuOpen, setDotMenuOpen] = useState(false);
|
||||
const [keyboardHelpOpen, setKeyboardHelpOpen] = useState(false);
|
||||
const [helpHelpActive] = useState(queryParams.has("helpHelp"));
|
||||
const [userId, setUserId] = useState(loggedInUser?.email);
|
||||
const [userId, setUserId] = useState(user?.email);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
setUserId(loggedInUser?.email);
|
||||
}, [loggedInUser]);
|
||||
|
||||
setUserId(user?.email)
|
||||
}, [user]);
|
||||
|
||||
|
||||
const [googleAnalyticsUtils] = useState(new GoogleAnalyticsUtils());
|
||||
|
||||
/*******************************************************************************
|
||||
@ -619,35 +687,9 @@ export default function App({authenticationMetaData}: Props)
|
||||
*******************************************************************************/
|
||||
function recordAnalytics(model: AnalyticsModel)
|
||||
{
|
||||
googleAnalyticsUtils.recordAnalytics(model);
|
||||
googleAnalyticsUtils.recordAnalytics(model)
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////
|
||||
// if any of the auth/session setup code determined that we need //
|
||||
// to render something and return early - then do so here. //
|
||||
///////////////////////////////////////////////////////////////////
|
||||
if (earlyReturnForAuth)
|
||||
{
|
||||
return (earlyReturnForAuth);
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
function banner(): JSX.Element | null
|
||||
{
|
||||
const banner = getBanner(metaData?.branding, "QFMD_TOP_OF_SITE");
|
||||
|
||||
if (!banner)
|
||||
{
|
||||
return (null);
|
||||
}
|
||||
|
||||
return (<Box className={getBannerClassName(banner)} sx={{display: "flex", justifyContent: "center", padding: "0.5rem", position: "sticky", top: "0", zIndex: 1, ...getBannerStyles(banner)}}>
|
||||
{makeBannerContent(banner)}
|
||||
</Box>);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@ -676,7 +718,6 @@ export default function App({authenticationMetaData}: Props)
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<CommandMenu metaData={metaData} />
|
||||
{banner()}
|
||||
<Sidenav
|
||||
color={sidenavColor}
|
||||
icon={branding.icon}
|
||||
@ -686,7 +727,6 @@ export default function App({authenticationMetaData}: Props)
|
||||
routes={sideNavRoutes}
|
||||
onMouseEnter={handleOnMouseEnter}
|
||||
onMouseLeave={handleOnMouseLeave}
|
||||
logout={doLogout}
|
||||
/>
|
||||
<Routes>
|
||||
<Route path="*" element={<Navigate to={defaultRoute} />} />
|
||||
|
149
src/index.tsx
149
src/index.tsx
@ -19,97 +19,116 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Auth0Provider} from "@auth0/auth0-react";
|
||||
import {QAuthenticationMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAuthenticationMetaData";
|
||||
import React from "react";
|
||||
import {createRoot} from "react-dom/client";
|
||||
import {BrowserRouter, useNavigate, useSearchParams} from "react-router-dom";
|
||||
import App from "App";
|
||||
import "qqq/styles/qqq-override-styles.css";
|
||||
import "qqq/styles/globals.scss";
|
||||
import "qqq/styles/raycast.scss";
|
||||
import useAnonymousAuthenticationModule from "qqq/authorization/anonymous/useAnonymousAuthenticationModule";
|
||||
import useAuth0AuthenticationModule from "qqq/authorization/auth0/useAuth0AuthenticationModule";
|
||||
import useOAuth2AuthenticationModule from "qqq/authorization/oauth2/useOAuth2AuthenticationModule";
|
||||
import HandleAuthorizationError from "HandleAuthorizationError";
|
||||
import ProtectedRoute from "qqq/authorization/auth0/ProtectedRoute";
|
||||
import {MaterialUIControllerProvider} from "qqq/context";
|
||||
import Client from "qqq/utils/qqq/Client";
|
||||
import React from "react";
|
||||
import {createRoot} from "react-dom/client";
|
||||
import {BrowserRouter} from "react-router-dom";
|
||||
|
||||
|
||||
const qController = Client.getInstance();
|
||||
|
||||
if (document.location.search && document.location.search.indexOf("clearAuthenticationMetaDataLocalStorage") > -1)
|
||||
if(document.location.search && document.location.search.indexOf("clearAuthenticationMetaDataLocalStorage") > -1)
|
||||
{
|
||||
qController.clearAuthenticationMetaDataLocalStorage();
|
||||
qController.clearAuthenticationMetaDataLocalStorage()
|
||||
}
|
||||
|
||||
const authenticationMetaDataPromise: Promise<QAuthenticationMetaData> = qController.getAuthenticationMetaData();
|
||||
const authenticationMetaDataPromise: Promise<QAuthenticationMetaData> = qController.getAuthenticationMetaData()
|
||||
|
||||
authenticationMetaDataPromise.then((authenticationMetaData) =>
|
||||
{
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
function Auth0RouterBody()
|
||||
// @ts-ignore
|
||||
function Auth0ProviderWithRedirectCallback({children, ...props})
|
||||
{
|
||||
const {renderAppWrapper} = useAuth0AuthenticationModule({});
|
||||
return (renderAppWrapper(authenticationMetaData));
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
// @ts-ignore
|
||||
const onRedirectCallback = (appState) =>
|
||||
{
|
||||
navigate((appState && appState.returnTo) || window.location.pathname);
|
||||
};
|
||||
if (searchParams.get("error"))
|
||||
{
|
||||
return (
|
||||
// @ts-ignore
|
||||
<Auth0Provider {...props}>
|
||||
<HandleAuthorizationError errorMessage={searchParams.get("error_description")} />
|
||||
</Auth0Provider>
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
return (
|
||||
// @ts-ignore
|
||||
<Auth0Provider onRedirectCallback={onRedirectCallback} {...props}>
|
||||
{children}
|
||||
</Auth0Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
function OAuth2RouterBody()
|
||||
{
|
||||
const {renderAppWrapper} = useOAuth2AuthenticationModule({inOAuthContext: false});
|
||||
return (renderAppWrapper(authenticationMetaData, (
|
||||
<MaterialUIControllerProvider>
|
||||
<App authenticationMetaData={authenticationMetaData} />
|
||||
</MaterialUIControllerProvider>
|
||||
)));
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
function AnonymousRouterBody()
|
||||
{
|
||||
const {renderAppWrapper} = useAnonymousAuthenticationModule({});
|
||||
return (renderAppWrapper(authenticationMetaData, (
|
||||
<MaterialUIControllerProvider>
|
||||
<App authenticationMetaData={authenticationMetaData} />
|
||||
</MaterialUIControllerProvider>
|
||||
)));
|
||||
}
|
||||
|
||||
|
||||
const container = document.getElementById("root");
|
||||
const root = createRoot(container);
|
||||
|
||||
if (authenticationMetaData.type === "AUTH_0")
|
||||
{
|
||||
root.render(<BrowserRouter>
|
||||
<Auth0RouterBody />
|
||||
</BrowserRouter>);
|
||||
}
|
||||
else if (authenticationMetaData.type === "OAUTH2")
|
||||
{
|
||||
root.render(<BrowserRouter>
|
||||
<OAuth2RouterBody />
|
||||
</BrowserRouter>);
|
||||
}
|
||||
else if (authenticationMetaData.type === "FULLY_ANONYMOUS" || authenticationMetaData.type === "MOCK")
|
||||
{
|
||||
root.render(<BrowserRouter>
|
||||
<AnonymousRouterBody />
|
||||
</BrowserRouter>);
|
||||
// @ts-ignore
|
||||
let domain: string = authenticationMetaData.data.baseUrl;
|
||||
|
||||
// @ts-ignore
|
||||
const clientId = authenticationMetaData.data.clientId;
|
||||
|
||||
// @ts-ignore
|
||||
const audience = authenticationMetaData.data.audience;
|
||||
|
||||
if(!domain || !clientId)
|
||||
{
|
||||
root.render(
|
||||
<div>Error: AUTH0 authenticationMetaData is missing domain [{domain}] and/or clientId [{clientId}].</div>
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if(domain.endsWith("/"))
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
// auth0 lib fails if we have a trailing slash. be a bit more graceful than that. //
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
domain = domain.replace(/\/$/, "");
|
||||
}
|
||||
|
||||
root.render(
|
||||
<BrowserRouter>
|
||||
<Auth0ProviderWithRedirectCallback
|
||||
domain={domain}
|
||||
clientId={clientId}
|
||||
audience={audience}
|
||||
redirectUri={`${window.location.origin}/`}
|
||||
>
|
||||
<MaterialUIControllerProvider>
|
||||
<ProtectedRoute component={App} />
|
||||
</MaterialUIControllerProvider>
|
||||
</Auth0ProviderWithRedirectCallback>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
root.render(<div>
|
||||
Error: Unknown authenticationMetaData type: [{authenticationMetaData.type}].
|
||||
</div>);
|
||||
root.render(
|
||||
<BrowserRouter>
|
||||
<MaterialUIControllerProvider>
|
||||
<App />
|
||||
</MaterialUIControllerProvider>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
})
|
||||
|
@ -1,82 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. 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 {QAuthenticationMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAuthenticationMetaData";
|
||||
import {SESSION_UUID_COOKIE_NAME} from "App";
|
||||
import Client from "qqq/utils/qqq/Client";
|
||||
import {useCookies} from "react-cookie";
|
||||
import {Md5} from "ts-md5/dist/md5";
|
||||
|
||||
const qController = Client.getInstance();
|
||||
|
||||
interface Props
|
||||
{
|
||||
setIsFullyAuthenticated?: (is: boolean) => void;
|
||||
setLoggedInUser?: (user: any) => void;
|
||||
setEarlyReturnForAuth?: (element: JSX.Element | null) => void;
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
** hook for working with the anonymous authentication module
|
||||
***************************************************************************/
|
||||
export default function useAnonymousAuthenticationModule({setIsFullyAuthenticated, setLoggedInUser, setEarlyReturnForAuth}: Props)
|
||||
{
|
||||
const [cookies, setCookie, removeCookie] = useCookies([SESSION_UUID_COOKIE_NAME]);
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
const setupSession = async () =>
|
||||
{
|
||||
console.log("Generating random token...");
|
||||
setIsFullyAuthenticated(true);
|
||||
qController.setGotAuthentication();
|
||||
setCookie(SESSION_UUID_COOKIE_NAME, Md5.hashStr(`${new Date()}`), {path: "/"});
|
||||
console.log("Token generation complete.");
|
||||
};
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
const logout = () =>
|
||||
{
|
||||
qController.clearAuthenticationMetaDataLocalStorage();
|
||||
removeCookie(SESSION_UUID_COOKIE_NAME, {path: "/"});
|
||||
};
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
const renderAppWrapper = (authenticationMetaData: QAuthenticationMetaData, children: JSX.Element): JSX.Element =>
|
||||
{
|
||||
return children;
|
||||
};
|
||||
|
||||
|
||||
return {
|
||||
setupSession,
|
||||
logout,
|
||||
renderAppWrapper
|
||||
};
|
||||
|
||||
}
|
@ -1,252 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. 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 {Auth0Provider, useAuth0} from "@auth0/auth0-react";
|
||||
import {QAuthenticationMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAuthenticationMetaData";
|
||||
import App, {SESSION_UUID_COOKIE_NAME} from "App";
|
||||
import HandleAuthorizationError from "HandleAuthorizationError";
|
||||
import jwt_decode from "jwt-decode";
|
||||
import ProtectedRoute from "qqq/authorization/auth0/ProtectedRoute";
|
||||
import {MaterialUIControllerProvider} from "qqq/context";
|
||||
import Client from "qqq/utils/qqq/Client";
|
||||
import {useCookies} from "react-cookie";
|
||||
import {useNavigate, useSearchParams} from "react-router-dom";
|
||||
|
||||
const qController = Client.getInstance();
|
||||
|
||||
interface Props
|
||||
{
|
||||
setIsFullyAuthenticated?: (is: boolean) => void;
|
||||
setLoggedInUser?: (user: any) => void;
|
||||
setEarlyReturnForAuth?: (element: JSX.Element | null) => void;
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
** hook for working with the Auth0 authentication module
|
||||
***************************************************************************/
|
||||
export default function useAuth0AuthenticationModule({setIsFullyAuthenticated, setLoggedInUser}: Props)
|
||||
{
|
||||
const {user: auth0User, getAccessTokenSilently: auth0GetAccessTokenSilently, logout: useAuth0Logout} = useAuth0();
|
||||
|
||||
const [cookies, removeCookie] = useCookies([SESSION_UUID_COOKIE_NAME]);
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
const shouldStoreNewToken = (newToken: string, oldToken: string): boolean =>
|
||||
{
|
||||
if (!cookies[SESSION_UUID_COOKIE_NAME])
|
||||
{
|
||||
console.log("No session uuid cookie - so we should store a new one.");
|
||||
return (true);
|
||||
}
|
||||
|
||||
if (!oldToken)
|
||||
{
|
||||
console.log("No accessToken in localStorage - so we should store a new one.");
|
||||
return (true);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
const oldJSON: any = jwt_decode(oldToken);
|
||||
const newJSON: any = jwt_decode(newToken);
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
// if the old (local storage) token is expired, then we need to store the new one //
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
const oldExp = oldJSON["exp"];
|
||||
if (oldExp * 1000 < (new Date().getTime()))
|
||||
{
|
||||
console.log("Access token in local storage was expired - so we should store a new one.");
|
||||
return (true);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// remove the exp & iat values from what we compare - as they are always different from auth0 //
|
||||
// note, this is only deleting them from what we compare, not from what we'd store. //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
delete newJSON["exp"];
|
||||
delete newJSON["iat"];
|
||||
delete oldJSON["exp"];
|
||||
delete oldJSON["iat"];
|
||||
|
||||
const different = JSON.stringify(newJSON) !== JSON.stringify(oldJSON);
|
||||
if (different)
|
||||
{
|
||||
console.log("Latest access token from auth0 has changed vs localStorage - so we should store a new one.");
|
||||
}
|
||||
return (different);
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
console.log("Caught in shouldStoreNewToken: " + e);
|
||||
}
|
||||
|
||||
return (true);
|
||||
};
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
const setupSession = async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
console.log("Loading token from auth0...");
|
||||
const accessToken = await auth0GetAccessTokenSilently();
|
||||
|
||||
const lsAccessToken = localStorage.getItem("accessToken");
|
||||
if (shouldStoreNewToken(accessToken, lsAccessToken))
|
||||
{
|
||||
console.log("Sending accessToken to backend, requesting a sessionUUID...");
|
||||
const {uuid: values} = await qController.manageSession(accessToken, null);
|
||||
|
||||
localStorage.setItem("accessToken", accessToken);
|
||||
localStorage.setItem("sessionValues", JSON.stringify(values));
|
||||
console.log("Got new sessionUUID from backend, and stored new accessToken");
|
||||
}
|
||||
else
|
||||
{
|
||||
console.log("Using existing sessionUUID cookie");
|
||||
}
|
||||
|
||||
setIsFullyAuthenticated(true);
|
||||
qController.setGotAuthentication();
|
||||
|
||||
setLoggedInUser(auth0User);
|
||||
console.log("Token load complete.");
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
console.log(`Error loading token: ${JSON.stringify(e)}`);
|
||||
qController.clearAuthenticationMetaDataLocalStorage();
|
||||
localStorage.removeItem("accessToken");
|
||||
removeCookie(SESSION_UUID_COOKIE_NAME, {path: "/"});
|
||||
useAuth0Logout();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
const logout = () =>
|
||||
{
|
||||
useAuth0Logout({returnTo: window.location.origin});
|
||||
};
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
// @ts-ignore
|
||||
function Auth0ProviderWithRedirectCallback({children, ...props})
|
||||
{
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
// @ts-ignore
|
||||
const onRedirectCallback = (appState) =>
|
||||
{
|
||||
navigate((appState && appState.returnTo) || window.location.pathname);
|
||||
};
|
||||
if (searchParams.get("error"))
|
||||
{
|
||||
return (
|
||||
// @ts-ignore
|
||||
<Auth0Provider {...props}>
|
||||
<HandleAuthorizationError errorMessage={searchParams.get("error_description")} />
|
||||
</Auth0Provider>
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
return (
|
||||
// @ts-ignore
|
||||
<Auth0Provider onRedirectCallback={onRedirectCallback} {...props}>
|
||||
{children}
|
||||
</Auth0Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
const renderAppWrapper = (authenticationMetaData: QAuthenticationMetaData): JSX.Element =>
|
||||
{
|
||||
// @ts-ignore
|
||||
let domain: string = authenticationMetaData.data.baseUrl;
|
||||
|
||||
// @ts-ignore
|
||||
const clientId = authenticationMetaData.data.clientId;
|
||||
|
||||
// @ts-ignore
|
||||
const audience = authenticationMetaData.data.audience;
|
||||
|
||||
if (!domain || !clientId)
|
||||
{
|
||||
return (
|
||||
<div>Error: AUTH0 authenticationMetaData is missing baseUrl [{domain}] and/or clientId [{clientId}].</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (domain.endsWith("/"))
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
// auth0 lib fails if we have a trailing slash. be a bit more graceful than that. //
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
domain = domain.replace(/\/$/, "");
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
** simple Functional Component to wrap the <App> and pass the authentication-
|
||||
** MetaData prop in, so a simple Component can be passed into ProtectedRoute
|
||||
***************************************************************************/
|
||||
function WrappedApp()
|
||||
{
|
||||
return <App authenticationMetaData={authenticationMetaData} />
|
||||
}
|
||||
|
||||
return (
|
||||
<Auth0ProviderWithRedirectCallback
|
||||
domain={domain}
|
||||
clientId={clientId}
|
||||
audience={audience}
|
||||
redirectUri={`${window.location.origin}/`}>
|
||||
<MaterialUIControllerProvider>
|
||||
<ProtectedRoute component={WrappedApp} />
|
||||
</MaterialUIControllerProvider>
|
||||
</Auth0ProviderWithRedirectCallback>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
return {
|
||||
setupSession,
|
||||
logout,
|
||||
renderAppWrapper
|
||||
};
|
||||
|
||||
}
|
@ -1,188 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. 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 {QAuthenticationMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAuthenticationMetaData";
|
||||
import {SESSION_UUID_COOKIE_NAME} from "App";
|
||||
import Client from "qqq/utils/qqq/Client";
|
||||
import {useCookies} from "react-cookie";
|
||||
import {AuthContextProps, AuthProvider, useAuth} from "react-oidc-context";
|
||||
import {useNavigate, useSearchParams} from "react-router-dom";
|
||||
|
||||
const qController = Client.getInstance();
|
||||
|
||||
interface Props
|
||||
{
|
||||
setIsFullyAuthenticated?: (is: boolean) => void;
|
||||
setLoggedInUser?: (user: any) => void;
|
||||
setEarlyReturnForAuth?: (element: JSX.Element | null) => void;
|
||||
inOAuthContext: boolean;
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
** hook for working with the OAuth2 authentication module
|
||||
***************************************************************************/
|
||||
export default function useOAuth2AuthenticationModule({setIsFullyAuthenticated, setLoggedInUser, setEarlyReturnForAuth, inOAuthContext}: Props)
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
// the useAuth hook should only be called if we're inside the <AuthProvider> element //
|
||||
// so on the page that uses this hook to call renderAppWrapper, we aren't in that //
|
||||
// element/context, thus, don't call that hook. //
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
const authOidc: AuthContextProps | null = inOAuthContext ? useAuth() : null;
|
||||
|
||||
const [cookies, removeCookie] = useCookies([SESSION_UUID_COOKIE_NAME]);
|
||||
const [searchParams] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
const setupSession = async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
const preSigninRedirectPathnameKey = "oauth2.preSigninRedirect.pathname";
|
||||
if (window.location.pathname == "/token")
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// if we're at a path of /token, get code & state params, look up values //
|
||||
// from that state in local storage, and make a post to the backend to //
|
||||
// with these values - which will itself talk to the identity provider //
|
||||
// to get an access token, and ultimately a session. //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
const code = searchParams.get("code");
|
||||
const state = searchParams.get("state");
|
||||
const oidcString = localStorage.getItem(`oidc.${state}`);
|
||||
if (oidcString)
|
||||
{
|
||||
const oidcObject = JSON.parse(oidcString) as { [name: string]: any };
|
||||
console.log(oidcObject);
|
||||
const manageSessionRequestBody = {code: code, codeVerifier: oidcObject.code_verifier, redirectUri: oidcObject.redirect_uri};
|
||||
const {uuid: newSessionUuid, values} = await qController.manageSession(null, null, manageSessionRequestBody);
|
||||
console.log(`we have new session UUID: ${newSessionUuid}`);
|
||||
|
||||
setIsFullyAuthenticated(true);
|
||||
qController.setGotAuthentication();
|
||||
|
||||
setLoggedInUser(values?.user);
|
||||
console.log("Token load complete.");
|
||||
|
||||
const preSigninRedirectPathname = localStorage.getItem(preSigninRedirectPathnameKey);
|
||||
localStorage.removeItem(preSigninRedirectPathname);
|
||||
navigate(preSigninRedirectPathname ?? "/", {replace: true});
|
||||
}
|
||||
else
|
||||
{
|
||||
////////////////////////////////////////////
|
||||
// if unrecognized state, render an error //
|
||||
////////////////////////////////////////////
|
||||
setEarlyReturnForAuth(<div>Login error: Unrecognized state. Refresh to try again.</div>);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// if we have a sessionUUID cookie, try to validate it with the backend //
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
const sessionUuid = cookies[SESSION_UUID_COOKIE_NAME];
|
||||
if (sessionUuid)
|
||||
{
|
||||
console.log(`we have session UUID: ${sessionUuid} - validating it...`);
|
||||
const {values} = await qController.manageSession(null, sessionUuid, null);
|
||||
|
||||
setIsFullyAuthenticated(true);
|
||||
qController.setGotAuthentication();
|
||||
|
||||
setLoggedInUser(values?.user);
|
||||
console.log("Token load complete.");
|
||||
}
|
||||
else
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// else no cookie, and not a token url, we need to redirect to the provider's login page //
|
||||
// capture the path the user was trying to access in local storage, to redirect back to later. //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
console.log("Loading token from OAuth2 provider...");
|
||||
console.log(authOidc);
|
||||
localStorage.setItem(preSigninRedirectPathnameKey, window.location.pathname);
|
||||
setEarlyReturnForAuth(<div>Signing in...</div>);
|
||||
authOidc?.signinRedirect();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
console.log(`Error loading token: ${JSON.stringify(e)}`);
|
||||
logout();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
const logout = () =>
|
||||
{
|
||||
qController.clearAuthenticationMetaDataLocalStorage();
|
||||
removeCookie(SESSION_UUID_COOKIE_NAME, {path: "/"});
|
||||
authOidc?.signoutRedirect();
|
||||
};
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
const renderAppWrapper = (authenticationMetaData: QAuthenticationMetaData, children: JSX.Element): JSX.Element =>
|
||||
{
|
||||
const authority: string = authenticationMetaData.data.baseUrl;
|
||||
const clientId = authenticationMetaData.data.clientId;
|
||||
|
||||
if (!authority || !clientId)
|
||||
{
|
||||
return (
|
||||
<div>Error: OAuth2 authenticationMetaData is missing baseUrl [{authority}] and/or clientId [{clientId}].</div>
|
||||
);
|
||||
}
|
||||
|
||||
const oidcConfig =
|
||||
{
|
||||
authority: authority,
|
||||
client_id: clientId,
|
||||
redirect_uri: `${window.location.origin}/token`,
|
||||
response_type: "code",
|
||||
scope: "openid profile email offline_access",
|
||||
};
|
||||
|
||||
return (<AuthProvider {...oidcConfig}>
|
||||
{children}
|
||||
</AuthProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
return {
|
||||
setupSession,
|
||||
logout,
|
||||
renderAppWrapper
|
||||
};
|
||||
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* Copyright (C) 2021-2022. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
@ -19,18 +19,20 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.frontend.materialdashboard.model.metadata;
|
||||
import {useAuth0} from "@auth0/auth0-react";
|
||||
import {Button} from "@mui/material";
|
||||
import React from "react";
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.branding.BannerSlot;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public enum MaterialDashboardBannerSlots implements BannerSlot
|
||||
function AuthenticationButton()
|
||||
{
|
||||
QFMD_TOP_OF_SITE,
|
||||
QFMD_TOP_OF_BODY,
|
||||
QFMD_SIDE_NAV_UNDER_LOGO
|
||||
const {loginWithRedirect, logout, isAuthenticated} = useAuth0();
|
||||
|
||||
if (isAuthenticated)
|
||||
{
|
||||
return <Button onClick={() => logout({returnTo: window.location.origin})}>Log Out</Button>;
|
||||
}
|
||||
|
||||
return <Button onClick={() => loginWithRedirect()}>Log In</Button>;
|
||||
}
|
||||
|
||||
export default AuthenticationButton;
|
@ -96,7 +96,6 @@ function QDynamicForm({formData, formLabel, bulkEditMode, bulkEditSwitchChangeHa
|
||||
if (width == "full")
|
||||
{
|
||||
itemSM = 12;
|
||||
itemLG = 12;
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -270,7 +270,7 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
|
||||
{
|
||||
pageHeader &&
|
||||
<Box display="flex" justifyContent="space-between">
|
||||
<MDTypography pb="0.5rem" variant="h3" color={light ? "white" : "dark"} noWrap>
|
||||
<MDTypography pb="0.5rem" textTransform="capitalize" variant="h3" color={light ? "white" : "dark"} noWrap>
|
||||
{pageHeader}
|
||||
</MDTypography>
|
||||
</Box>
|
||||
|
@ -20,22 +20,21 @@
|
||||
*/
|
||||
|
||||
import {QBrandingMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QBrandingMetaData";
|
||||
import {Button} from "@mui/material";
|
||||
import Box from "@mui/material/Box";
|
||||
import Divider from "@mui/material/Divider";
|
||||
import Icon from "@mui/material/Icon";
|
||||
import Link from "@mui/material/Link";
|
||||
import List from "@mui/material/List";
|
||||
import {ReactNode, useEffect, useReducer, useState} from "react";
|
||||
import {NavLink, useLocation} from "react-router-dom";
|
||||
import AuthenticationButton from "qqq/components/buttons/AuthenticationButton";
|
||||
import SideNavCollapse from "qqq/components/horseshoe/sidenav/SideNavCollapse";
|
||||
import SideNavItem from "qqq/components/horseshoe/sidenav/SideNavItem";
|
||||
import SideNavList from "qqq/components/horseshoe/sidenav/SideNavList";
|
||||
import SidenavRoot from "qqq/components/horseshoe/sidenav/SideNavRoot";
|
||||
import sidenavLogoLabel from "qqq/components/horseshoe/sidenav/styles/SideNav";
|
||||
import MDTypography from "qqq/components/legacy/MDTypography";
|
||||
import {getBannerClassName, getBannerStyles, getBanner, makeBannerContent} from "qqq/components/misc/Banners";
|
||||
import {setMiniSidenav, setTransparentSidenav, setWhiteSidenav, useMaterialUIController,} from "qqq/context";
|
||||
import {ReactNode, useEffect, useReducer, useState} from "react";
|
||||
import {NavLink, useLocation} from "react-router-dom";
|
||||
|
||||
|
||||
interface Props
|
||||
@ -45,7 +44,6 @@ interface Props
|
||||
logo?: string;
|
||||
appName?: string;
|
||||
branding?: QBrandingMetaData;
|
||||
logout: () => void;
|
||||
routes: {
|
||||
[key: string]:
|
||||
| ReactNode
|
||||
@ -68,7 +66,7 @@ interface Props
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
function Sidenav({color, icon, logo, appName, branding, routes, logout, ...rest}: Props): JSX.Element
|
||||
function Sidenav({color, icon, logo, appName, branding, routes, ...rest}: Props): JSX.Element
|
||||
{
|
||||
const [openCollapse, setOpenCollapse] = useState<boolean | string>(false);
|
||||
const [openNestedCollapse, setOpenNestedCollapse] = useState<boolean | string>(false);
|
||||
@ -259,7 +257,7 @@ function Sidenav({color, icon, logo, appName, branding, routes, logout, ...rest}
|
||||
active={key === collapseName}
|
||||
open={openCollapse === key}
|
||||
noCollapse={noCollapse}
|
||||
onClick={() => (!noCollapse ? (openCollapse === key ? setOpenCollapse(false) : setOpenCollapse(key)) : null)}
|
||||
onClick={() => (! noCollapse ? (openCollapse === key ? setOpenCollapse(false) : setOpenCollapse(key)) : null) }
|
||||
>
|
||||
{collapse ? renderCollapse(collapse) : null}
|
||||
</SideNavCollapse>
|
||||
@ -302,30 +300,6 @@ function Sidenav({color, icon, logo, appName, branding, routes, logout, ...rest}
|
||||
}
|
||||
);
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
function EnvironmentBanner({branding}: { branding: QBrandingMetaData }): JSX.Element | null
|
||||
{
|
||||
// deprecated!
|
||||
if (branding && branding.environmentBannerText)
|
||||
{
|
||||
return <Box mt={2} bgcolor={branding.environmentBannerColor} borderRadius={2}>
|
||||
{branding.environmentBannerText}
|
||||
</Box>;
|
||||
}
|
||||
|
||||
const banner = getBanner(branding, "QFMD_SIDE_NAV_UNDER_LOGO");
|
||||
if (banner)
|
||||
{
|
||||
return <Box className={getBannerClassName(banner)} mt={2} borderRadius={2} sx={getBannerStyles(banner)}>
|
||||
{makeBannerContent(banner)}
|
||||
</Box>;
|
||||
}
|
||||
|
||||
return (null);
|
||||
}
|
||||
|
||||
return (
|
||||
<SidenavRoot
|
||||
{...rest}
|
||||
@ -356,7 +330,12 @@ function Sidenav({color, icon, logo, appName, branding, routes, logout, ...rest}
|
||||
</Box>
|
||||
}
|
||||
</Box>
|
||||
<EnvironmentBanner branding={branding} />
|
||||
{
|
||||
branding && branding.environmentBannerText &&
|
||||
<Box mt={2} bgcolor={branding.environmentBannerColor} borderRadius={2}>
|
||||
{branding.environmentBannerText}
|
||||
</Box>
|
||||
}
|
||||
</Box>
|
||||
<Divider
|
||||
light={
|
||||
@ -371,7 +350,7 @@ function Sidenav({color, icon, logo, appName, branding, routes, logout, ...rest}
|
||||
(darkMode && !transparentSidenav && whiteSidenav)
|
||||
}
|
||||
/>
|
||||
<Button onClick={logout}>Log Out</Button>
|
||||
<AuthenticationButton />
|
||||
</SidenavRoot>
|
||||
);
|
||||
}
|
||||
|
@ -97,7 +97,6 @@ export default styled(Drawer)(({theme, ownerState}: { theme?: Theme | any; owner
|
||||
margin: "0",
|
||||
borderRadius: "0",
|
||||
height: "100%",
|
||||
top: "unset",
|
||||
|
||||
...(miniSidenav ? drawerCloseStyles() : drawerOpenStyles()),
|
||||
},
|
||||
|
@ -1,97 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. 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 {Banner} from "@kingsrook/qqq-frontend-core/lib/model/metaData/Banner";
|
||||
import {QBrandingMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QBrandingMetaData";
|
||||
import parse from "html-react-parser";
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// One may render a banner using the functions in this file as: //
|
||||
// //
|
||||
// const banner = getBanner(branding, "QFMD_SIDE_NAV_UNDER_LOGO"); //
|
||||
// return (<Box className={getBannerClassName(banner)} sx={{padding: "1rem", ...getBannerStyles(banner)}}> //
|
||||
// {makeBannerContent(banner)} //
|
||||
// </Box>); //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
export function getBanner(branding: QBrandingMetaData, slot: string): Banner | null
|
||||
{
|
||||
if (branding?.banners?.has(slot))
|
||||
{
|
||||
return (branding.banners.get(slot));
|
||||
}
|
||||
|
||||
return (null);
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
export function getBannerStyles(banner: Banner)
|
||||
{
|
||||
let bgColor = "";
|
||||
let color = "";
|
||||
|
||||
if (banner)
|
||||
{
|
||||
if (banner.backgroundColor)
|
||||
{
|
||||
bgColor = banner.backgroundColor;
|
||||
}
|
||||
|
||||
if (banner.textColor)
|
||||
{
|
||||
bgColor = banner.textColor;
|
||||
}
|
||||
}
|
||||
|
||||
const rest = banner?.additionalStyles ?? {};
|
||||
|
||||
return ({
|
||||
backgroundColor: bgColor,
|
||||
color: color,
|
||||
...rest
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
export function getBannerClassName(banner: Banner)
|
||||
{
|
||||
return `banner ${banner?.severity?.toLowerCase()}`;
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
export function makeBannerContent(banner: Banner): JSX.Element
|
||||
{
|
||||
return <>{banner?.messageHTML ? parse(banner?.messageHTML) : banner?.messageText}</>;
|
||||
}
|
||||
|
@ -20,7 +20,6 @@
|
||||
*/
|
||||
|
||||
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
|
||||
import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType";
|
||||
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
|
||||
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
||||
import {QTableVariant} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableVariant";
|
||||
@ -36,11 +35,12 @@ import DialogTitle from "@mui/material/DialogTitle";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import Icon from "@mui/material/Icon";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import {any} from "prop-types";
|
||||
import React, {useState} from "react";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
import {QCancelButton} from "qqq/components/buttons/DefaultButtons";
|
||||
import MDButton from "qqq/components/legacy/MDButton";
|
||||
import Client from "qqq/utils/qqq/Client";
|
||||
import React, {useState} from "react";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
|
||||
interface Props
|
||||
{
|
||||
@ -162,8 +162,8 @@ function GotoRecordDialog(props: Props): JSX.Element
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** event handler for close button
|
||||
***************************************************************************/
|
||||
** event handler for close button
|
||||
***************************************************************************/
|
||||
const closeRequested = () =>
|
||||
{
|
||||
if (props.mayClose)
|
||||
@ -182,23 +182,23 @@ function GotoRecordDialog(props: Props): JSX.Element
|
||||
|
||||
options[optionIndex].forEach((field) =>
|
||||
{
|
||||
if (values[field.name])
|
||||
if(values[field.name])
|
||||
{
|
||||
anyFieldsInThisOptionHaveAValue = true;
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
if (!anyFieldsInThisOptionHaveAValue)
|
||||
if(!anyFieldsInThisOptionHaveAValue)
|
||||
{
|
||||
return (true);
|
||||
}
|
||||
return (false);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** event handler for clicking an 'option's go/submit button
|
||||
***************************************************************************/
|
||||
** event handler for clicking an 'option's go/submit button
|
||||
***************************************************************************/
|
||||
const optionGoClicked = async (optionIndex: number) =>
|
||||
{
|
||||
setError("");
|
||||
@ -207,13 +207,9 @@ function GotoRecordDialog(props: Props): JSX.Element
|
||||
const queryStringParts: string[] = [];
|
||||
options[optionIndex].forEach((field) =>
|
||||
{
|
||||
if (field.type == QFieldType.STRING && !values[field.name])
|
||||
{
|
||||
return;
|
||||
}
|
||||
criteria.push(new QFilterCriteria(field.name, QCriteriaOperator.EQUALS, [values[field.name]]));
|
||||
queryStringParts.push(`${field.name}=${encodeURIComponent(values[field.name])}`);
|
||||
});
|
||||
criteria.push(new QFilterCriteria(field.name, QCriteriaOperator.EQUALS, [values[field.name]]))
|
||||
queryStringParts.push(`${field.name}=${encodeURIComponent(values[field.name])}`)
|
||||
})
|
||||
|
||||
const filter = new QQueryFilter(criteria, null, null, "AND", null, 10);
|
||||
|
||||
@ -227,7 +223,7 @@ function GotoRecordDialog(props: Props): JSX.Element
|
||||
}
|
||||
else if (queryResult.length == 1)
|
||||
{
|
||||
if (options[optionIndex].length == 1 && options[optionIndex][0].name == pkey?.name)
|
||||
if(options[optionIndex].length == 1 && options[optionIndex][0].name == pkey?.name)
|
||||
{
|
||||
/////////////////////////////////////////////////
|
||||
// navigate by pkey, if that's how we searched //
|
||||
|
@ -109,7 +109,6 @@ export const getOperatorOptions = (tableMetaData: QTableMetaData, fieldName: str
|
||||
{
|
||||
case QFieldType.DECIMAL:
|
||||
case QFieldType.INTEGER:
|
||||
case QFieldType.LONG:
|
||||
operatorOptions.push({label: "equals", value: QCriteriaOperator.EQUALS, valueMode: ValueMode.SINGLE});
|
||||
operatorOptions.push({label: "does not equal", value: QCriteriaOperator.NOT_EQUALS_OR_IS_NULL, valueMode: ValueMode.SINGLE});
|
||||
operatorOptions.push({label: "greater than", value: QCriteriaOperator.GREATER_THAN, valueMode: ValueMode.SINGLE});
|
||||
|
@ -111,7 +111,7 @@ function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRo
|
||||
}
|
||||
|
||||
const tableMetaData = data.childTableMetaData instanceof QTableMetaData ? data.childTableMetaData as QTableMetaData : new QTableMetaData(data.childTableMetaData);
|
||||
const rows = DataGridUtils.makeRows(records, tableMetaData, undefined, true);
|
||||
const rows = DataGridUtils.makeRows(records, tableMetaData, true);
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
// note - tablePath may be null, if the user doesn't have access to the table. //
|
||||
@ -255,14 +255,14 @@ function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRo
|
||||
disabledFields = data.defaultValuesForNewChildRecords;
|
||||
}
|
||||
|
||||
const defaultValuesForNewChildRecords = data.defaultValuesForNewChildRecords || {};
|
||||
const defaultValuesForNewChildRecords = data.defaultValuesForNewChildRecords || {}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
// copy values from specified fields in the parent record down into the child record //
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
if (data.defaultValuesForNewChildRecordsFromParentFields)
|
||||
if(data.defaultValuesForNewChildRecordsFromParentFields)
|
||||
{
|
||||
for (let childField in data.defaultValuesForNewChildRecordsFromParentFields)
|
||||
for(let childField in data.defaultValuesForNewChildRecordsFromParentFields)
|
||||
{
|
||||
const parentField = data.defaultValuesForNewChildRecordsFromParentFields[childField];
|
||||
defaultValuesForNewChildRecords[childField] = parentRecord?.values?.get(parentField);
|
||||
|
@ -21,12 +21,11 @@
|
||||
|
||||
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
|
||||
import Box from "@mui/material/Box";
|
||||
import {ReactNode, useEffect, useState} from "react";
|
||||
import Footer from "qqq/components/horseshoe/Footer";
|
||||
import NavBar from "qqq/components/horseshoe/NavBar";
|
||||
import {getBannerClassName, getBannerStyles, getBanner, makeBannerContent} from "qqq/components/misc/Banners";
|
||||
import DashboardLayout from "qqq/layouts/DashboardLayout";
|
||||
import Client from "qqq/utils/qqq/Client";
|
||||
import {ReactNode, useEffect, useState} from "react";
|
||||
|
||||
interface Props
|
||||
{
|
||||
@ -81,34 +80,12 @@ function BaseLayout({stickyNavbar, children}: Props): JSX.Element
|
||||
return () => window.removeEventListener("resize", handleTabsOrientation);
|
||||
}, [tabsOrientation]);
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
function banner(): JSX.Element | null
|
||||
{
|
||||
const banner = getBanner(metaData?.branding, "QFMD_TOP_OF_BODY");
|
||||
|
||||
if (!banner)
|
||||
{
|
||||
return (null);
|
||||
}
|
||||
|
||||
return (<Box className={getBannerClassName(banner)} sx={{display: "flex", justifyContent: "center", padding: "0.5rem", margin: "-20px", marginBottom: "20px", ...getBannerStyles(banner)}}>
|
||||
{makeBannerContent(banner)}
|
||||
</Box>);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<DashboardLayout>
|
||||
{banner()}
|
||||
<NavBar />
|
||||
<Box>{children}</Box>
|
||||
<Footer company={{href: metaData?.branding?.companyUrl, name: metaData?.branding?.companyName}} />
|
||||
</DashboardLayout>
|
||||
</>
|
||||
<DashboardLayout>
|
||||
<NavBar />
|
||||
<Box>{children}</Box>
|
||||
<Footer company={{href: metaData?.branding?.companyUrl, name: metaData?.branding?.companyName}} />
|
||||
</DashboardLayout>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1838,20 +1838,6 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
return;
|
||||
}
|
||||
|
||||
if(urlSearchParams.get("defaultProcessValues"))
|
||||
{
|
||||
if(!defaultProcessValues)
|
||||
{
|
||||
defaultProcessValues = {}
|
||||
}
|
||||
|
||||
const values = JSON.parse(urlSearchParams.get("defaultProcessValues"));
|
||||
for (let key in values)
|
||||
{
|
||||
defaultProcessValues[key] = values[key]
|
||||
}
|
||||
}
|
||||
|
||||
if (defaultProcessValues)
|
||||
{
|
||||
for (let key in defaultProcessValues)
|
||||
|
@ -1103,7 +1103,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
|
||||
////////////////////////////////
|
||||
// make the rows for the grid //
|
||||
////////////////////////////////
|
||||
const rows = DataGridUtils.makeRows(results, tableMetaData, tableVariant);
|
||||
const rows = DataGridUtils.makeRows(results, tableMetaData);
|
||||
setRows(rows);
|
||||
|
||||
setLoading(false);
|
||||
@ -1612,22 +1612,6 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
|
||||
*******************************************************************************/
|
||||
const processClicked = (process: QProcessMetaData) =>
|
||||
{
|
||||
if (process.minInputRecords != null && process.minInputRecords > 0 && getNoOfSelectedRecords() === 0)
|
||||
{
|
||||
setAlertContent(`No records were selected for the process: ${process.label}`);
|
||||
return;
|
||||
}
|
||||
else if (process.minInputRecords != null && getNoOfSelectedRecords() < process.minInputRecords)
|
||||
{
|
||||
setAlertContent(`Too few records were selected for the process: ${process.label}. A minimum of ${process.minInputRecords} is required.`);
|
||||
return;
|
||||
}
|
||||
else if (process.maxInputRecords != null && getNoOfSelectedRecords() > process.maxInputRecords)
|
||||
{
|
||||
setAlertContent(`Too many records were selected for the process: ${process.label}. A maximum of ${process.maxInputRecords} is allowed.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// todo - let the process specify that it needs initial rows - err if none selected.
|
||||
// alternatively, let a process itself have an initial screen to select rows...
|
||||
openModalProcess(process);
|
||||
|
@ -92,7 +92,7 @@ const TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT = "qqq.tableVariant";
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
export function renderSectionOfFields(key: string, fieldNames: string[], tableMetaData: QTableMetaData, helpHelpActive: boolean, record: QRecord, fieldMap?: { [name: string]: QFieldMetaData }, styleOverrides?: {label?: SxProps, value?: SxProps}, tableVariant?: QTableVariant)
|
||||
export function renderSectionOfFields(key: string, fieldNames: string[], tableMetaData: QTableMetaData, helpHelpActive: boolean, record: QRecord, fieldMap?: { [name: string]: QFieldMetaData }, styleOverrides?: { label?: SxProps, value?: SxProps })
|
||||
{
|
||||
return <Grid container lg={12} key={key} display="flex" py={1} pr={2}>
|
||||
{
|
||||
@ -119,7 +119,7 @@ export function renderSectionOfFields(key: string, fieldNames: string[], tableMe
|
||||
}
|
||||
<div style={{display: "inline-block", width: 0}}> </div>
|
||||
<Typography variant="button" textTransform="none" fontWeight="regular" color="rgb(123, 128, 154)" sx={{...(styleOverrides?.value ?? {})}}>
|
||||
{ValueUtils.getDisplayValue(field, record, "view", fieldName, tableVariant)}
|
||||
{ValueUtils.getDisplayValue(field, record, "view", fieldName)}
|
||||
</Typography>
|
||||
</>
|
||||
</Grid>
|
||||
@ -598,7 +598,7 @@ function RecordView({table, record: overrideRecord, launchProcess}: Props): JSX.
|
||||
// for a section with field names, render the field values. //
|
||||
// for the T1 section, the "wrapper" will come out below - but for other sections, produce a wrapper too. //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
const fields = renderSectionOfFields(section.name, section.fieldNames, tableMetaData, helpHelpActive, record, undefined, undefined, tableVariant);
|
||||
const fields = renderSectionOfFields(section.name, section.fieldNames, tableMetaData, helpHelpActive, record);
|
||||
|
||||
if (section.tier === "T1")
|
||||
{
|
||||
|
@ -748,54 +748,35 @@ input[type="search"]::-webkit-search-results-decoration
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.helpContentAlert.info,
|
||||
.banner.info
|
||||
{
|
||||
background-color: rgb(234, 242, 255);
|
||||
color: rgb(20, 51, 102);
|
||||
}
|
||||
|
||||
.helpContentAlert.info .MuiAlert-icon .material-icons-round,
|
||||
.banner.info .MuiAlert-icon .material-icons-round
|
||||
{
|
||||
color: #0062FF;
|
||||
}
|
||||
|
||||
.helpContentAlert.success,
|
||||
.banner.success
|
||||
.helpContentAlert.success
|
||||
{
|
||||
background-color: rgb(240, 248, 241);
|
||||
color: rgb(44, 76, 46);
|
||||
}
|
||||
|
||||
.helpContentAlert.success .MuiAlert-icon .material-icons-round,
|
||||
.banner.success .MuiAlert-icon .material-icons-round
|
||||
.helpContentAlert.success .MuiAlert-icon .material-icons-round
|
||||
{
|
||||
color: #4CAF50;
|
||||
}
|
||||
|
||||
.helpContentAlert.warning,
|
||||
.banner.warning
|
||||
.helpContentAlert.warning
|
||||
{
|
||||
background-color: rgb(254, 245, 234);
|
||||
color: rgb(100, 65, 20);
|
||||
}
|
||||
|
||||
.helpContentAlert.warning .MuiAlert-icon .material-icons-round,
|
||||
.banner.warning .MuiAlert-icon .material-icons-round
|
||||
.helpContentAlert.warning .MuiAlert-icon .material-icons-round
|
||||
{
|
||||
color: #fb8c00;
|
||||
}
|
||||
|
||||
.helpContentAlert.error,
|
||||
.banner.error
|
||||
.helpContentAlert.error
|
||||
{
|
||||
background-color: rgb(254, 239, 238);
|
||||
color: rgb(98, 41, 37);
|
||||
}
|
||||
|
||||
.helpContentAlert.error .MuiAlert-icon .material-icons-round,
|
||||
.banner.error .MuiAlert-icon .material-icons-round
|
||||
.helpContentAlert.error .MuiAlert-icon .material-icons-round
|
||||
{
|
||||
color: #F44335;
|
||||
}
|
||||
|
@ -24,7 +24,6 @@ import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QF
|
||||
import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType";
|
||||
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
|
||||
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
||||
import {QTableVariant} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableVariant";
|
||||
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
||||
import Tooltip from "@mui/material/Tooltip/Tooltip";
|
||||
import {GridColDef, GridRowsProp, MuiEvent} from "@mui/x-data-grid-pro";
|
||||
@ -71,7 +70,7 @@ export default class DataGridUtils
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static makeRows = (results: QRecord[], tableMetaData: QTableMetaData, tableVariant?: QTableVariant, allowEmptyId = false): GridRowsProp[] =>
|
||||
public static makeRows = (results: QRecord[], tableMetaData: QTableMetaData, allowEmptyId = false): GridRowsProp[] =>
|
||||
{
|
||||
const fields = [...tableMetaData.fields.values()];
|
||||
const rows = [] as any[];
|
||||
@ -83,7 +82,7 @@ export default class DataGridUtils
|
||||
|
||||
fields.forEach((field) =>
|
||||
{
|
||||
row[field.name] = ValueUtils.getDisplayValue(field, record, "query", undefined, tableVariant);
|
||||
row[field.name] = ValueUtils.getDisplayValue(field, record, "query");
|
||||
});
|
||||
|
||||
if (tableMetaData.exposedJoins)
|
||||
@ -98,7 +97,7 @@ export default class DataGridUtils
|
||||
fields.forEach((field) =>
|
||||
{
|
||||
let fieldName = join.joinTable.name + "." + field.name;
|
||||
row[fieldName] = ValueUtils.getDisplayValue(field, record, "query", fieldName, tableVariant);
|
||||
row[fieldName] = ValueUtils.getDisplayValue(field, record, "query", fieldName);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -102,7 +102,7 @@ export default class GoogleAnalyticsUtils
|
||||
console.log("Error reading session values from localStorage: " + e);
|
||||
}
|
||||
|
||||
if (this.metaData.environmentValues?.get("GOOGLE_ANALYTICS_ENABLED") == "true" && this.metaData.environmentValues?.get("GOOGLE_ANALYTICS_TRACKING_ID"))
|
||||
if (this.metaData.environmentValues.get("GOOGLE_ANALYTICS_ENABLED") == "true" && this.metaData.environmentValues.get("GOOGLE_ANALYTICS_TRACKING_ID"))
|
||||
{
|
||||
this.active = true;
|
||||
|
||||
|
@ -23,7 +23,6 @@ import {AdornmentType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/Ado
|
||||
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
|
||||
import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType";
|
||||
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
|
||||
import {QTableVariant} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableVariant";
|
||||
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
||||
import "datejs"; // https://github.com/datejs/Datejs
|
||||
import {Chip, ClickAwayListener, Icon} from "@mui/material";
|
||||
@ -77,14 +76,14 @@ class ValueUtils
|
||||
** When you have a field, and a record - call this method to get a string or
|
||||
** element back to display the field's value.
|
||||
*******************************************************************************/
|
||||
public static getDisplayValue(field: QFieldMetaData, record: QRecord, usage: "view" | "query" = "view", overrideFieldName?: string, tableVariant?: QTableVariant): string | JSX.Element | JSX.Element[]
|
||||
public static getDisplayValue(field: QFieldMetaData, record: QRecord, usage: "view" | "query" = "view", overrideFieldName?: string): string | JSX.Element | JSX.Element[]
|
||||
{
|
||||
const fieldName = overrideFieldName ?? field.name;
|
||||
|
||||
const displayValue = record.displayValues ? record.displayValues.get(fieldName) : undefined;
|
||||
const rawValue = record.values ? record.values.get(fieldName) : undefined;
|
||||
|
||||
return ValueUtils.getValueForDisplay(field, rawValue, displayValue, usage, tableVariant, record, fieldName);
|
||||
return ValueUtils.getValueForDisplay(field, rawValue, displayValue, usage);
|
||||
}
|
||||
|
||||
|
||||
@ -92,35 +91,14 @@ class ValueUtils
|
||||
** When you have a field and a value (either just a raw value, or a raw and
|
||||
** display value), call this method to get a string Element to display.
|
||||
*******************************************************************************/
|
||||
public static getValueForDisplay(field: QFieldMetaData, rawValue: any, displayValue: any = rawValue, usage: "view" | "query" = "view", tableVariant?: QTableVariant, record?: QRecord, fieldName?: string): string | JSX.Element | JSX.Element[]
|
||||
public static getValueForDisplay(field: QFieldMetaData, rawValue: any, displayValue: any = rawValue, usage: "view" | "query" = "view"): string | JSX.Element | JSX.Element[]
|
||||
{
|
||||
if (field.hasAdornment(AdornmentType.LINK))
|
||||
{
|
||||
const adornment = field.getAdornment(AdornmentType.LINK);
|
||||
let href = String(rawValue);
|
||||
|
||||
let toRecordFromTable = adornment.getValue("toRecordFromTable");
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
// if the link adornment has a 'toRecordFromTableDynamic', then look for a display //
|
||||
// value named `fieldName`:toRecordFromTableDynamic for the table name. //
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
if(adornment.getValue("toRecordFromTableDynamic"))
|
||||
{
|
||||
const toRecordFromTableDynamic = record?.displayValues?.get(fieldName + ":toRecordFromTableDynamic");
|
||||
if(toRecordFromTableDynamic)
|
||||
{
|
||||
toRecordFromTable = toRecordFromTableDynamic;
|
||||
}
|
||||
else
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////
|
||||
// if the table name isn't known, then return w/o the adornment. //
|
||||
///////////////////////////////////////////////////////////////////
|
||||
return (ValueUtils.getUnadornedValueForDisplay(field, rawValue, displayValue));
|
||||
}
|
||||
}
|
||||
let href = rawValue;
|
||||
|
||||
const toRecordFromTable = adornment.getValue("toRecordFromTable");
|
||||
if (toRecordFromTable)
|
||||
{
|
||||
if (ValueUtils.getQInstance())
|
||||
@ -129,7 +107,7 @@ class ValueUtils
|
||||
if (!tablePath)
|
||||
{
|
||||
console.log("Couldn't find path for table: " + toRecordFromTable);
|
||||
return (ValueUtils.getUnadornedValueForDisplay(field, rawValue, displayValue));
|
||||
return (displayValue ?? rawValue);
|
||||
}
|
||||
|
||||
if (!tablePath.endsWith("/"))
|
||||
@ -221,44 +199,12 @@ class ValueUtils
|
||||
|
||||
if (field.type == QFieldType.BLOB || field.hasAdornment(AdornmentType.FILE_DOWNLOAD))
|
||||
{
|
||||
let url = rawValue;
|
||||
if(tableVariant)
|
||||
{
|
||||
url += "?tableVariant=" + encodeURIComponent(JSON.stringify(tableVariant));
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// if the field has the download adornment with a downloadUrlDynamic value, //
|
||||
// then get the url from a displayValue of `fieldName`:downloadUrlDynamic. //
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
if(field.hasAdornment(AdornmentType.FILE_DOWNLOAD))
|
||||
{
|
||||
const adornment = field.getAdornment(AdornmentType.FILE_DOWNLOAD);
|
||||
let downloadUrlDynamicAdornmentValue = adornment.getValue("downloadUrlDynamic");
|
||||
if(downloadUrlDynamicAdornmentValue)
|
||||
{
|
||||
const downloadUrlDynamicValue = record?.displayValues?.get(fieldName + ":downloadUrlDynamic");
|
||||
if (downloadUrlDynamicValue)
|
||||
{
|
||||
url = downloadUrlDynamicValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
////////////////////////////////////////////////////////////////
|
||||
// if the url isn't available, then return w/o the adornment. //
|
||||
////////////////////////////////////////////////////////////////
|
||||
return (ValueUtils.getUnadornedValueForDisplay(field, rawValue, displayValue));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (<BlobComponent field={field} url={url} filename={displayValue} usage={usage} />);
|
||||
return (<BlobComponent field={field} url={rawValue} filename={displayValue} usage={usage} />);
|
||||
}
|
||||
|
||||
return (ValueUtils.getUnadornedValueForDisplay(field, rawValue, displayValue));
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** After we know there's no element to be returned (e.g., because no adornment),
|
||||
** this method does the string formatting.
|
||||
@ -267,13 +213,7 @@ class ValueUtils
|
||||
{
|
||||
if (!displayValue && field.defaultValue)
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// note, at one point in time, we used a field's default value here if no displayValue... but that feels 100% wrong, //
|
||||
// e.g., a null field would show up (on a query or view screen) has having some value! //
|
||||
// not sure if this was maybe supposed to be displayValue = rawValue, but, keep that in mind, and keep this block here //
|
||||
// in case we run into issues and need to revisit/rethink //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// displayValue = field.defaultValue;
|
||||
displayValue = field.defaultValue;
|
||||
}
|
||||
|
||||
if (field.type === QFieldType.DATE_TIME)
|
||||
|
Reference in New Issue
Block a user