diff --git a/package.json b/package.json
index bb9072d..cea87a5 100644
--- a/package.json
+++ b/package.json
@@ -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.94",
+ "@kingsrook/qqq-frontend-core": "1.0.96",
"@mui/icons-material": "5.4.1",
"@mui/material": "5.11.1",
"@mui/styles": "5.11.1",
@@ -44,6 +44,7 @@
"react-dnd": "16.0.1",
"react-dnd-html5-backend": "16.0.1",
"react-dom": "18.0.0",
+ "react-ga4": "2.1.0",
"react-github-btn": "1.2.1",
"react-google-drive-picker": "^1.2.0",
"react-markdown": "9.0.1",
diff --git a/src/App.tsx b/src/App.tsx
index 56484e6..4b68292 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -33,12 +33,8 @@ 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 jwt_decode from "jwt-decode";
-import React, {JSXElementConstructor, Key, ReactElement, useEffect, useState,} from "react";
-import {useCookies} from "react-cookie";
-import {Navigate, Route, Routes, useLocation, useSearchParams,} from "react-router-dom";
-import {Md5} from "ts-md5/dist/md5";
import CommandMenu from "CommandMenu";
+import jwt_decode from "jwt-decode";
import QContext from "QContext";
import Sidenav from "qqq/components/horseshoe/sidenav/SideNav";
import theme from "qqq/components/legacy/Theme";
@@ -53,8 +49,13 @@ import EntityEdit from "qqq/pages/records/edit/RecordEdit";
import RecordQuery from "qqq/pages/records/query/RecordQuery";
import RecordDeveloperView from "qqq/pages/records/view/RecordDeveloperView";
import RecordView from "qqq/pages/records/view/RecordView";
+import GoogleAnalyticsUtils from "qqq/utils/GoogleAnalyticsUtils";
import Client from "qqq/utils/qqq/Client";
import ProcessUtils from "qqq/utils/qqq/ProcessUtils";
+import React, {JSXElementConstructor, Key, ReactElement, useEffect, useState,} from "react";
+import {useCookies} from "react-cookie";
+import {Navigate, Route, Routes, useLocation, useSearchParams,} from "react-router-dom";
+import {Md5} from "ts-md5/dist/md5";
const qController = Client.getInstance();
@@ -160,7 +161,7 @@ export default function App()
if (shouldStoreNewToken(accessToken, lsAccessToken))
{
console.log("Sending accessToken to backend, requesting a sessionUUID...");
- const newSessionUuid = await qController.manageSession(accessToken, null);
+ 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. //
@@ -168,6 +169,7 @@ export default function App()
// 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
@@ -654,7 +656,7 @@ export default function App()
},
);
- const [pageHeader, setPageHeader] = useState("" as string | JSX.Element);
+ const [pageHeader, setPageHeaderState] = useState("" as string | JSX.Element);
const [accentColor, setAccentColor] = useState("#0062FF");
const [accentColorLight, setAccentColorLight] = useState("#C0D6F7");
const [tableMetaData, setTableMetaData] = useState(null);
@@ -663,6 +665,35 @@ export default function App()
const [keyboardHelpOpen, setKeyboardHelpOpen] = useState(false);
const [helpHelpActive] = useState(queryParams.has("helpHelp"));
+ const [googleAnalyticsUtils] = useState(new GoogleAnalyticsUtils());
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ function setPageHeader(header: string | JSX.Element)
+ {
+ setPageHeaderState(header);
+ if(typeof header == "string")
+ {
+ recordAnalytics(header)
+ }
+ else
+ {
+ recordAnalytics("Title not available")
+ }
+ }
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ function recordAnalytics(title: string)
+ {
+ googleAnalyticsUtils.recordAnalytics(location, title)
+ }
+
+
return (
appRoutes && (
@@ -682,6 +713,7 @@ export default function App()
setTableProcesses: (tableProcesses: QProcessMetaData[]) => setTableProcesses(tableProcesses),
setDotMenuOpen: (dotMenuOpent: boolean) => setDotMenuOpen(dotMenuOpent),
setKeyboardHelpOpen: (keyboardHelpOpen: boolean) => setKeyboardHelpOpen(keyboardHelpOpen),
+ recordAnalytics: recordAnalytics,
pathToLabelMap: pathToLabelMap,
branding: branding
}}>
diff --git a/src/QContext.tsx b/src/QContext.tsx
index 797c970..3352816 100644
--- a/src/QContext.tsx
+++ b/src/QContext.tsx
@@ -47,6 +47,11 @@ interface QContext
tableProcesses?: QProcessMetaData[];
setTableProcesses?: (tableProcesses: QProcessMetaData[]) => void;
+ ///////////////////////////////////////////
+ // function to record an analytics event //
+ ///////////////////////////////////////////
+ recordAnalytics?: (title: string) => void;
+
///////////////////////////////////
// constants - no setters needed //
///////////////////////////////////
diff --git a/src/qqq/pages/records/query/RecordQuery.tsx b/src/qqq/pages/records/query/RecordQuery.tsx
index 1c5e016..5742ba0 100644
--- a/src/qqq/pages/records/query/RecordQuery.tsx
+++ b/src/qqq/pages/records/query/RecordQuery.tsx
@@ -623,25 +623,25 @@ const RecordQuery = forwardRef(({table, usage, isModal}: Props, ref) =>
if (validType && !dotMenuOpen && !keyboardHelpOpen && !activeModalProcess)
{
- if (!e.metaKey && e.key === "n" && table.capabilities.has(Capability.TABLE_INSERT) && table.insertPermission)
+ if (!e.metaKey && !e.ctrlKey && e.key === "n" && table.capabilities.has(Capability.TABLE_INSERT) && table.insertPermission)
{
e.preventDefault();
navigate(`${metaData?.getTablePathByName(tableName)}/create`);
}
- else if (!e.metaKey && e.key === "r")
+ else if (!e.metaKey && !e.ctrlKey && e.key === "r")
{
e.preventDefault();
updateTable("'r' keyboard event");
}
/*
// disable until we add a ... ref down to let us programmatically open Columns button
- else if (! e.metaKey && e.key === "c")
+ else if (! e.metaKey && !e.ctrlKey && e.key === "c")
{
e.preventDefault()
gridApiRef.current.showPreferences(GridPreferencePanelsValue.columns)
}
*/
- else if (!e.metaKey && e.key === "f")
+ else if (!e.metaKey && !e.ctrlKey && e.key === "f")
{
e.preventDefault();
diff --git a/src/qqq/pages/records/view/RecordView.tsx b/src/qqq/pages/records/view/RecordView.tsx
index 455ac6b..b678b8f 100644
--- a/src/qqq/pages/records/view/RecordView.tsx
+++ b/src/qqq/pages/records/view/RecordView.tsx
@@ -164,27 +164,27 @@ function RecordView({table, launchProcess}: Props): JSX.Element
if (validType && !dotMenuOpen && !keyboardHelpOpen && !showAudit && !showEditChildForm)
{
- if (!e.metaKey && e.key === "n" && table.capabilities.has(Capability.TABLE_INSERT) && table.insertPermission)
+ if (!e.metaKey && !e.ctrlKey && e.key === "n" && table.capabilities.has(Capability.TABLE_INSERT) && table.insertPermission)
{
e.preventDefault();
gotoCreate();
}
- else if (!e.metaKey && e.key === "e" && table.capabilities.has(Capability.TABLE_UPDATE) && table.editPermission)
+ else if (!e.metaKey && !e.ctrlKey && e.key === "e" && table.capabilities.has(Capability.TABLE_UPDATE) && table.editPermission)
{
e.preventDefault();
navigate("edit");
}
- else if (!e.metaKey && e.key === "c" && table.capabilities.has(Capability.TABLE_INSERT) && table.insertPermission)
+ else if (!e.metaKey && !e.ctrlKey && e.key === "c" && table.capabilities.has(Capability.TABLE_INSERT) && table.insertPermission)
{
e.preventDefault();
navigate("copy");
}
- else if (!e.metaKey && e.key === "d" && table.capabilities.has(Capability.TABLE_DELETE) && table.deletePermission)
+ else if (!e.metaKey && !e.ctrlKey && e.key === "d" && table.capabilities.has(Capability.TABLE_DELETE) && table.deletePermission)
{
e.preventDefault();
handleClickDeleteButton();
}
- else if (!e.metaKey && e.key === "a" && metaData && metaData.tables.has("audit"))
+ else if (!e.metaKey && !e.ctrlKey && e.key === "a" && metaData && metaData.tables.has("audit"))
{
e.preventDefault();
navigate("#audit");
diff --git a/src/qqq/utils/GoogleAnalyticsUtils.ts b/src/qqq/utils/GoogleAnalyticsUtils.ts
new file mode 100644
index 0000000..3f5a894
--- /dev/null
+++ b/src/qqq/utils/GoogleAnalyticsUtils.ts
@@ -0,0 +1,115 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
+import Client from "qqq/utils/qqq/Client";
+import ReactGA from "react-ga4";
+
+
+const qController = Client.getInstance();
+
+/*******************************************************************************
+ ** Utilities for working with Google Analytics (through react-ga4)^
+ *******************************************************************************/
+export default class GoogleAnalyticsUtils
+{
+ private metaData: QInstance = null;
+ private active: boolean = false;
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ constructor()
+ {
+ }
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private send = (location: Location, title: string) =>
+ {
+ if(!this.active)
+ {
+ return;
+ }
+
+ ReactGA.send({hitType: "pageview", page: location.pathname + location.search, title: title});
+ }
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private setup = async (): Promise =>
+ {
+ this.metaData = await qController.loadMetaData();
+
+ let sessionValues: {[key: string]: any} = null;
+ try
+ {
+ sessionValues = JSON.parse(localStorage.getItem("sessionValues"));
+ }
+ catch(e)
+ {
+ 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"))
+ {
+ this.active = true;
+
+ if(sessionValues && sessionValues["googleAnalyticsValues"])
+ {
+ ReactGA.gtag("set", "user_properties", sessionValues["googleAnalyticsValues"]);
+ }
+
+ ReactGA.initialize(this.metaData.environmentValues.get("GOOGLE_ANALYTICS_TRACKING_ID"),
+ {
+ gaOptions: {},
+ gtagOptions: {}
+ });
+ }
+ else
+ {
+ this.active = false;
+ }
+ }
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public recordAnalytics = (location: Location, title: string) =>
+ {
+ if(this.metaData == null)
+ {
+ (async () =>
+ {
+ await this.setup();
+ })()
+ }
+
+ this.send(location, title);
+ }
+
+}
\ No newline at end of file