mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-17 21:00:45 +00:00
Merged main into feature/CE-881-create-basic-saved-reports
This commit is contained in:
@ -115,7 +115,7 @@ workflows:
|
||||
context: [ qqq-maven-registry-credentials, kingsrook-slack, build-qqq-sample-app ]
|
||||
filters:
|
||||
branches:
|
||||
ignore: /main/
|
||||
ignore: /(main|integration.*)/
|
||||
tags:
|
||||
ignore: /(version|snapshot)-.*/
|
||||
deploy:
|
||||
@ -124,7 +124,7 @@ workflows:
|
||||
context: [ qqq-maven-registry-credentials, kingsrook-slack, build-qqq-sample-app ]
|
||||
filters:
|
||||
branches:
|
||||
only: /main/
|
||||
only: /(main|integration.*)/
|
||||
tags:
|
||||
only: /(version|snapshot)-.*/
|
||||
|
||||
|
2200
package-lock.json
generated
2200
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -6,13 +6,14 @@
|
||||
"@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",
|
||||
"@mui/system": "5.11.1",
|
||||
"@mui/x-data-grid": "5.17.23",
|
||||
"@mui/x-data-grid-pro": "5.17.23",
|
||||
"@mui/x-date-pickers": "7.1.1",
|
||||
"@mui/x-license-pro": "5.12.3",
|
||||
"@react-jvectormap/core": "1.0.1",
|
||||
"@react-jvectormap/unitedstates": "1.0.1",
|
||||
@ -26,6 +27,7 @@
|
||||
"chroma-js": "2.4.2",
|
||||
"cmdk": "0.2.0",
|
||||
"datejs": "1.0.0-rc3",
|
||||
"dayjs": "1.11.10",
|
||||
"downshift": "3.2.10",
|
||||
"faker": "5.5.3",
|
||||
"form-data": "4.0.0",
|
||||
@ -42,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",
|
||||
|
55
src/App.tsx
55
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, {AnalyticsModel} 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();
|
||||
@ -79,7 +80,7 @@ export default function App()
|
||||
Client.setUnauthorizedCallback(() =>
|
||||
{
|
||||
logout();
|
||||
})
|
||||
});
|
||||
|
||||
const shouldStoreNewToken = (newToken: string, oldToken: string): boolean =>
|
||||
{
|
||||
@ -104,7 +105,7 @@ export default function App()
|
||||
// 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()))
|
||||
if (oldExp * 1000 < (new Date().getTime()))
|
||||
{
|
||||
console.log("Access token in local storage was expired - so we should store a new one.");
|
||||
return (true);
|
||||
@ -114,21 +115,21 @@ export default function App()
|
||||
// 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"]
|
||||
delete newJSON["exp"];
|
||||
delete newJSON["iat"];
|
||||
delete oldJSON["exp"];
|
||||
delete oldJSON["iat"];
|
||||
|
||||
const different = JSON.stringify(newJSON) !== JSON.stringify(oldJSON);
|
||||
if(different)
|
||||
if (different)
|
||||
{
|
||||
console.log("Latest access token from auth0 has changed vs localStorage - so we should store a new one.");
|
||||
}
|
||||
return (different);
|
||||
}
|
||||
catch(e)
|
||||
catch (e)
|
||||
{
|
||||
console.log("Caught in shouldStoreNewToken: " + e)
|
||||
console.log("Caught in shouldStoreNewToken: " + e);
|
||||
}
|
||||
|
||||
return (true);
|
||||
@ -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
|
||||
@ -185,7 +187,7 @@ export default function App()
|
||||
{
|
||||
console.log(`Error loading token: ${JSON.stringify(e)}`);
|
||||
qController.clearAuthenticationMetaDataLocalStorage();
|
||||
localStorage.removeItem("accessToken")
|
||||
localStorage.removeItem("accessToken");
|
||||
removeCookie(SESSION_UUID_COOKIE_NAME, {path: "/"});
|
||||
logout();
|
||||
return;
|
||||
@ -550,7 +552,7 @@ export default function App()
|
||||
});
|
||||
}
|
||||
|
||||
const pathToLabelMap: {[path: string]: string} = {}
|
||||
const pathToLabelMap: { [path: string]: string } = {};
|
||||
for (let i = 0; i < appRoutesList.length; i++)
|
||||
{
|
||||
const route = appRoutesList[i];
|
||||
@ -579,7 +581,7 @@ export default function App()
|
||||
{
|
||||
console.log("Exception is a QException with status = 401. Clearing some of localStorage & cookies");
|
||||
qController.clearAuthenticationMetaDataLocalStorage();
|
||||
localStorage.removeItem("accessToken")
|
||||
localStorage.removeItem("accessToken");
|
||||
removeCookie(SESSION_UUID_COOKIE_NAME, {path: "/"});
|
||||
|
||||
//////////////////////////////////////////////////////
|
||||
@ -656,13 +658,25 @@ export default function App()
|
||||
|
||||
const [pageHeader, setPageHeader] = useState("" as string | JSX.Element);
|
||||
const [accentColor, setAccentColor] = useState("#0062FF");
|
||||
const [accentColorLight, setAccentColorLight] = useState("#C0D6F7")
|
||||
const [accentColorLight, setAccentColorLight] = useState("#C0D6F7");
|
||||
const [tableMetaData, setTableMetaData] = useState(null);
|
||||
const [tableProcesses, setTableProcesses] = useState(null);
|
||||
const [dotMenuOpen, setDotMenuOpen] = useState(false);
|
||||
const [keyboardHelpOpen, setKeyboardHelpOpen] = useState(false);
|
||||
const [helpHelpActive] = useState(queryParams.has("helpHelp"));
|
||||
|
||||
const [googleAnalyticsUtils] = useState(new GoogleAnalyticsUtils());
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
function recordAnalytics(model: AnalyticsModel)
|
||||
{
|
||||
googleAnalyticsUtils.recordAnalytics(model)
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
|
||||
appRoutes && (
|
||||
@ -682,6 +696,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
|
||||
}}>
|
||||
@ -707,4 +722,4 @@ export default function App()
|
||||
</QContext.Provider>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -22,6 +22,7 @@
|
||||
import {QBrandingMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QBrandingMetaData";
|
||||
import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData";
|
||||
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
||||
import {AnalyticsModel} from "qqq/utils/GoogleAnalyticsUtils";
|
||||
import {createContext} from "react";
|
||||
|
||||
interface QContext
|
||||
@ -47,6 +48,11 @@ interface QContext
|
||||
tableProcesses?: QProcessMetaData[];
|
||||
setTableProcesses?: (tableProcesses: QProcessMetaData[]) => void;
|
||||
|
||||
///////////////////////////////////////////
|
||||
// function to record an analytics event //
|
||||
///////////////////////////////////////////
|
||||
recordAnalytics?: (model: AnalyticsModel) => void;
|
||||
|
||||
///////////////////////////////////
|
||||
// constants - no setters needed //
|
||||
///////////////////////////////////
|
||||
|
@ -94,7 +94,7 @@ function EntityForm(props: Props): JSX.Element
|
||||
const qController = Client.getInstance();
|
||||
const tableNameParam = useParams().tableName;
|
||||
const tableName = props.table === null ? tableNameParam : props.table.name;
|
||||
const {accentColor} = useContext(QContext);
|
||||
const {accentColor, recordAnalytics} = useContext(QContext);
|
||||
|
||||
const [formTitle, setFormTitle] = useState("");
|
||||
const [validations, setValidations] = useState({});
|
||||
@ -462,6 +462,7 @@ function EntityForm(props: Props): JSX.Element
|
||||
{
|
||||
const tableMetaData = await qController.loadTableMetaData(tableName);
|
||||
setTableMetaData(tableMetaData);
|
||||
recordAnalytics({location: window.location, title: (props.isCopy ? "Copy" : props.id ? "Edit" : "New") + ": " + tableMetaData.label});
|
||||
|
||||
setupFieldRules(tableMetaData);
|
||||
|
||||
@ -508,6 +509,7 @@ function EntityForm(props: Props): JSX.Element
|
||||
{
|
||||
record = await qController.get(tableName, props.id);
|
||||
setRecord(record);
|
||||
recordAnalytics({category: "tableEvents", action: props.isCopy ? "copy" : "edit", label: tableMetaData?.label + " / " + record?.recordLabel});
|
||||
|
||||
const titleVerb = props.isCopy ? "Copy" : "Edit";
|
||||
setFormTitle(`${titleVerb} ${tableMetaData?.label}: ${record?.recordLabel}`);
|
||||
@ -547,6 +549,7 @@ function EntityForm(props: Props): JSX.Element
|
||||
// else handle preparing to do an insert //
|
||||
///////////////////////////////////////////
|
||||
setFormTitle(`Creating New ${tableMetaData?.label}`);
|
||||
recordAnalytics({category: "tableEvents", action: "new", label: tableMetaData?.label});
|
||||
|
||||
if (!props.isModal)
|
||||
{
|
||||
@ -871,6 +874,8 @@ function EntityForm(props: Props): JSX.Element
|
||||
|
||||
if (props.id !== null && !props.isCopy)
|
||||
{
|
||||
recordAnalytics({category: "tableEvents", action: "saveEdit", label: tableMetaData?.label});
|
||||
|
||||
///////////////////////
|
||||
// perform an update //
|
||||
///////////////////////
|
||||
@ -913,6 +918,8 @@ function EntityForm(props: Props): JSX.Element
|
||||
}
|
||||
else
|
||||
{
|
||||
recordAnalytics({category: "tableEvents", action: props.isCopy ? "saveCopy" : "saveNew", label: tableMetaData?.label});
|
||||
|
||||
/////////////////////////////////
|
||||
// perform an insert //
|
||||
// todo - audit if it's a dupe //
|
||||
|
@ -23,8 +23,9 @@ import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QT
|
||||
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import {GridColDef, GridExportMenuItemProps} from "@mui/x-data-grid-pro";
|
||||
import React from "react";
|
||||
import QContext from "QContext";
|
||||
import ValueUtils from "qqq/utils/qqq/ValueUtils";
|
||||
import React, {useContext} from "react";
|
||||
|
||||
interface QExportMenuItemProps extends GridExportMenuItemProps<{}>
|
||||
{
|
||||
@ -43,6 +44,10 @@ export default function ExportMenuItem(props: QExportMenuItemProps)
|
||||
{
|
||||
const {format, tableMetaData, totalRecords, columnsModel, columnVisibilityModel, queryFilter, hideMenu} = props;
|
||||
|
||||
const {recordAnalytics} = useContext(QContext);
|
||||
|
||||
recordAnalytics({category: "tableEvents", action: "export", label: tableMetaData.label});
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
disabled={totalRecords === 0}
|
||||
|
@ -21,6 +21,7 @@
|
||||
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
||||
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
||||
import {Skeleton} from "@mui/material";
|
||||
import {Alert, Skeleton} from "@mui/material";
|
||||
import Box from "@mui/material/Box";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import Tab from "@mui/material/Tab";
|
||||
@ -95,9 +96,9 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
|
||||
|
||||
let initialSelectedTab = 0;
|
||||
let selectedTabKey: string = null;
|
||||
if(parentWidgetMetaData && wrapWidgetsInTabPanels)
|
||||
if (parentWidgetMetaData && wrapWidgetsInTabPanels)
|
||||
{
|
||||
selectedTabKey = `qqq.widgets.selectedTabs.${parentWidgetMetaData.name}`
|
||||
selectedTabKey = `qqq.widgets.selectedTabs.${parentWidgetMetaData.name}`;
|
||||
if (localStorage.getItem(selectedTabKey))
|
||||
{
|
||||
initialSelectedTab = Number(localStorage.getItem(selectedTabKey));
|
||||
@ -195,7 +196,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
|
||||
const metaDataToUse = (thisWidgetHasDropdowns) ? widgetMetaData : parentWidgetMetaData;
|
||||
for (let i = 0; i < metaDataToUse.dropdowns.length; i++)
|
||||
{
|
||||
const dropdownName = metaDataToUse.dropdowns[i].possibleValueSourceName;
|
||||
const dropdownName = metaDataToUse.dropdowns[i].possibleValueSourceName ?? metaDataToUse.dropdowns[i].name;
|
||||
const localStorageKey = `${WIDGET_DROPDOWN_SELECTION_LOCAL_STORAGE_KEY_ROOT}.${metaDataToUse.name}.${dropdownName}`;
|
||||
const json = JSON.parse(localStorage.getItem(localStorageKey));
|
||||
if (json)
|
||||
@ -307,6 +308,21 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
widgetMetaData.type === "alert" && widgetData[i]?.html && (
|
||||
<Widget
|
||||
omitPadding={true}
|
||||
widgetMetaData={widgetMetaData}
|
||||
widgetData={widgetData[i]}
|
||||
reloadWidgetCallback={(data) => reloadWidget(i, data)}
|
||||
isChild={areChildren}
|
||||
labelAdditionalComponentsRight={labelAdditionalComponentsRight}
|
||||
labelAdditionalComponentsLeft={labelAdditionalComponentsLeft}
|
||||
>
|
||||
<Alert severity={widgetData[i]?.alertType?.toLowerCase()}>{parse(widgetData[i]?.html)}</Alert>
|
||||
</Widget>
|
||||
)
|
||||
}
|
||||
{
|
||||
widgetMetaData.type === "usaMap" && (
|
||||
<USMapWidget
|
||||
@ -585,7 +601,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
|
||||
);
|
||||
};
|
||||
|
||||
if(wrapWidgetsInTabPanels)
|
||||
if (wrapWidgetsInTabPanels)
|
||||
{
|
||||
omitWrappingGridContainer = true;
|
||||
}
|
||||
@ -617,7 +633,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
|
||||
</TabPanel>);
|
||||
}
|
||||
|
||||
return (<React.Fragment key={`${widgetMetaData.name}-${i}`}>{renderedWidget}</React.Fragment>)
|
||||
return (<React.Fragment key={`${widgetMetaData.name}-${i}`}>{renderedWidget}</React.Fragment>);
|
||||
})
|
||||
}
|
||||
</>
|
||||
@ -625,7 +641,8 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
|
||||
|
||||
const tabs = widgetMetaDataList && wrapWidgetsInTabPanels ?
|
||||
<Tabs
|
||||
sx={{m: 0, mb: 1.5, ml: -2, mr: -2, mt: -3,
|
||||
sx={{
|
||||
m: 0, mb: 1.5, ml: -2, mr: -2, mt: -3,
|
||||
"& .MuiTabs-scroller": {
|
||||
ml: 0
|
||||
}
|
||||
@ -638,7 +655,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
|
||||
<Tab key={widgetMetaData.name} label={widgetMetaData.label} />
|
||||
))}
|
||||
</Tabs>
|
||||
: <></>
|
||||
: <></>;
|
||||
|
||||
return (
|
||||
widgetCount > 0 ? (
|
||||
|
@ -317,12 +317,20 @@ export class Dropdown extends LabelComponent
|
||||
if (localStorageOption)
|
||||
{
|
||||
const id = localStorageOption.id;
|
||||
for (let i = 0; i < this.options.length; i++)
|
||||
|
||||
if (this.dropdownMetaData.type == "DATE_PICKER")
|
||||
{
|
||||
if (this.options[i].id == id)
|
||||
defaultValue = id;
|
||||
}
|
||||
else
|
||||
{
|
||||
for (let i = 0; i < this.options.length; i++)
|
||||
{
|
||||
defaultValue = this.options[i];
|
||||
args.dropdownData[args.componentIndex] = defaultValue?.id;
|
||||
if (this.options[i].id == id)
|
||||
{
|
||||
defaultValue = this.options[i];
|
||||
args.dropdownData[args.componentIndex] = defaultValue?.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -376,6 +384,7 @@ export class Dropdown extends LabelComponent
|
||||
<Box mb={2} sx={{float: "right"}}>
|
||||
<WidgetDropdownMenu
|
||||
name={this.dropdownName}
|
||||
type={this.dropdownMetaData.type}
|
||||
defaultValue={defaultValue}
|
||||
sx={{marginLeft: "1rem"}}
|
||||
label={label}
|
||||
@ -702,11 +711,11 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
||||
setUsingLabelAsTitle(props.widgetData.isLabelPageTitle);
|
||||
}
|
||||
|
||||
const helpRoles = ["ALL_SCREENS"]
|
||||
const helpRoles = ["ALL_SCREENS"];
|
||||
const slotName = "label";
|
||||
const showHelp = helpHelpActive || hasHelpContent(props.widgetMetaData?.helpContent?.get(slotName), helpRoles);
|
||||
|
||||
if(showHelp)
|
||||
if (showHelp)
|
||||
{
|
||||
const formattedHelpContent = <HelpContent helpContents={props.widgetMetaData?.helpContent?.get(slotName)} roles={helpRoles} helpContentKey={`widget:${props.widgetMetaData?.name};slot:${slotName}`} />;
|
||||
labelElement = <Tooltip title={formattedHelpContent} arrow={true} placement="bottom-start">{labelElement}</Tooltip>;
|
||||
|
@ -31,7 +31,7 @@ export default function TextBlock({widgetMetaData, data}: StandardBlockComponent
|
||||
{
|
||||
return (
|
||||
<BlockElementWrapper metaData={widgetMetaData} data={data} slot="">
|
||||
<span>{data.values.text}</span>
|
||||
<span style={{fontSize: "1.000rem"}}>{data.values.text}</span>
|
||||
</BlockElementWrapper>
|
||||
);
|
||||
}
|
||||
|
@ -19,18 +19,23 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Collapse, Theme, InputAdornment} from "@mui/material";
|
||||
import {CalendarTodayOutlined} from "@mui/icons-material";
|
||||
import {Collapse, InputAdornment, Theme} from "@mui/material";
|
||||
import Autocomplete from "@mui/material/Autocomplete";
|
||||
import Box from "@mui/material/Box";
|
||||
import Icon from "@mui/material/Icon";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import {SxProps} from "@mui/system";
|
||||
import {DatePicker, DateValidationError, LocalizationProvider, PickerChangeHandlerContext, PickerValidDate} from "@mui/x-date-pickers";
|
||||
import {AdapterDayjs} from "@mui/x-date-pickers/AdapterDayjs";
|
||||
import dayjs from "dayjs";
|
||||
import {Field, Form, Formik} from "formik";
|
||||
import React, {useState} from "react";
|
||||
import QContext from "QContext";
|
||||
import colors from "qqq/assets/theme/base/colors";
|
||||
import MDInput from "qqq/components/legacy/MDInput";
|
||||
import ValueUtils from "qqq/utils/qqq/ValueUtils";
|
||||
import React, {useContext, useEffect, useState} from "react";
|
||||
|
||||
|
||||
export interface DropdownOption
|
||||
@ -45,6 +50,7 @@ export interface DropdownOption
|
||||
interface Props
|
||||
{
|
||||
name: string;
|
||||
type?: string;
|
||||
defaultValue?: any;
|
||||
label?: string;
|
||||
startIcon?: string;
|
||||
@ -96,7 +102,7 @@ function makeBackendValuesFromFrontendValues(frontendDefaultValues: StartAndEndD
|
||||
return (backendTimeValues);
|
||||
}
|
||||
|
||||
function WidgetDropdownMenu({name, defaultValue, label, startIcon, width, disableClearable, allowBackAndForth, backAndForthInverted, dropdownOptions, onChangeCallback, sx}: Props): JSX.Element
|
||||
function WidgetDropdownMenu({name, type, defaultValue, label, startIcon, width, disableClearable, allowBackAndForth, backAndForthInverted, dropdownOptions, onChangeCallback, sx}: Props): JSX.Element
|
||||
{
|
||||
const [customTimesVisible, setCustomTimesVisible] = useState(defaultValue && defaultValue.id && defaultValue.id.startsWith("custom,"));
|
||||
const [customTimeValuesFrontend, setCustomTimeValuesFrontend] = useState(parseCustomTimeValuesFromDefaultValue(defaultValue) as StartAndEndDate);
|
||||
@ -105,16 +111,27 @@ function WidgetDropdownMenu({name, defaultValue, label, startIcon, width, disabl
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [value, setValue] = useState(defaultValue);
|
||||
const [dateValue, setDateValue] = useState(defaultValue);
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
|
||||
const [backDisabled, setBackDisabled] = useState(false);
|
||||
const [forthDisabled, setForthDisabled] = useState(false);
|
||||
|
||||
const {accentColor} = useContext(QContext);
|
||||
|
||||
const doForceOpen = (event: React.MouseEvent<HTMLDivElement>) =>
|
||||
{
|
||||
setIsOpen(true);
|
||||
};
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if (type == "DATE_PICKER")
|
||||
{
|
||||
handleOnChange(null, defaultValue, null);
|
||||
}
|
||||
}, [defaultValue]);
|
||||
|
||||
function getSelectedIndex(value: DropdownOption)
|
||||
{
|
||||
let currentIndex = null;
|
||||
@ -129,9 +146,19 @@ function WidgetDropdownMenu({name, defaultValue, label, startIcon, width, disabl
|
||||
return currentIndex;
|
||||
}
|
||||
|
||||
const navigateBackAndForth = (event: React.MouseEvent, direction: -1 | 1) =>
|
||||
|
||||
const navigateBackAndForth = (event: React.MouseEvent, direction: -1 | 1, type: string) =>
|
||||
{
|
||||
event.stopPropagation();
|
||||
|
||||
if (type == "DATE_PICKER")
|
||||
{
|
||||
let currentDate = new Date(dateValue);
|
||||
currentDate.setDate(currentDate.getDate() + direction);
|
||||
handleOnChange(null, currentDate, null);
|
||||
return;
|
||||
}
|
||||
|
||||
let currentIndex = getSelectedIndex(value);
|
||||
|
||||
if (currentIndex == null)
|
||||
@ -156,9 +183,26 @@ function WidgetDropdownMenu({name, defaultValue, label, startIcon, width, disabl
|
||||
};
|
||||
|
||||
|
||||
const handleDatePickerOnChange = (value: PickerValidDate, context: PickerChangeHandlerContext<DateValidationError>) =>
|
||||
{
|
||||
if (value.isValid())
|
||||
{
|
||||
handleOnChange(null, value.toDate(), null);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleOnChange = (event: any, newValue: any, reason: string) =>
|
||||
{
|
||||
setValue(newValue);
|
||||
if (type == "DATE_PICKER")
|
||||
{
|
||||
setDateValue(newValue);
|
||||
newValue = {"id": new Date(newValue).toLocaleDateString()};
|
||||
}
|
||||
else
|
||||
{
|
||||
setValue(newValue);
|
||||
}
|
||||
|
||||
const isTimeframeCustom = name == "timeframe" && newValue && newValue.id == "custom";
|
||||
setCustomTimesVisible(isTimeframeCustom);
|
||||
@ -250,86 +294,123 @@ function WidgetDropdownMenu({name, defaultValue, label, startIcon, width, disabl
|
||||
|
||||
const fontSize = "1rem";
|
||||
let optionPaddingLeftRems = 0.75;
|
||||
if(startIcon)
|
||||
if (startIcon)
|
||||
{
|
||||
optionPaddingLeftRems += allowBackAndForth ? 1.5 : 1.75
|
||||
optionPaddingLeftRems += allowBackAndForth ? 1.5 : 1.75;
|
||||
}
|
||||
if(allowBackAndForth)
|
||||
if (allowBackAndForth)
|
||||
{
|
||||
optionPaddingLeftRems += 2.5;
|
||||
}
|
||||
|
||||
return (
|
||||
dropdownOptions ? (
|
||||
<Box sx={{whiteSpace: "nowrap", display: "flex",
|
||||
"& .MuiPopperUnstyled-root": {
|
||||
border: `1px solid ${colors.grayLines.main}`,
|
||||
borderTop: "none",
|
||||
borderRadius: "0 0 0.75rem 0.75rem",
|
||||
padding: 0,
|
||||
}, "& .MuiPaper-rounded": {
|
||||
borderRadius: "0 0 0.75rem 0.75rem",
|
||||
}
|
||||
}} className="dashboardDropdownMenu">
|
||||
<Autocomplete
|
||||
id={`${label}-combo-box`}
|
||||
|
||||
defaultValue={defaultValue}
|
||||
value={value}
|
||||
onChange={handleOnChange}
|
||||
inputValue={inputValue}
|
||||
onInputChange={handleOnInputChange}
|
||||
|
||||
isOptionEqualToValue={(option, value) => option.id === value.id}
|
||||
|
||||
open={isOpen}
|
||||
onOpen={() => setIsOpen(true)}
|
||||
onClose={() => setIsOpen(false)}
|
||||
|
||||
size="small"
|
||||
disablePortal
|
||||
disableClearable={disableClearable}
|
||||
options={dropdownOptions}
|
||||
sx={{
|
||||
...sx,
|
||||
cursor: "pointer",
|
||||
display: "inline-block",
|
||||
"& .MuiOutlinedInput-notchedOutline": {
|
||||
border: "none"
|
||||
},
|
||||
}}
|
||||
renderInput={(params: any) =>
|
||||
<>
|
||||
<Box sx={{width: `${width}px`, background: "white", borderRadius: isOpen ? "0.75rem 0.75rem 0 0" : "0.75rem", border: `1px solid ${colors.grayLines.main}`, "& *": {cursor: "pointer"}}} display="flex" alignItems="center" onClick={(event) => doForceOpen(event)}>
|
||||
{allowBackAndForth && <IconButton onClick={(event) => navigateBackAndForth(event, backAndForthInverted ? 1 : -1)} disabled={backDisabled}><Icon>navigate_before</Icon></IconButton>}
|
||||
<TextField {...params} placeholder={label} sx={{
|
||||
"& .MuiInputBase-input": {
|
||||
fontSize: fontSize
|
||||
}
|
||||
}} InputProps={{...params.InputProps, startAdornment: startAdornment/*, endAdornment: endAdornment*/}}
|
||||
/>
|
||||
{allowBackAndForth && <IconButton onClick={(event) => navigateBackAndForth(event, backAndForthInverted ? -1 : 1)} disabled={forthDisabled}><Icon>navigate_next</Icon></IconButton>}
|
||||
</Box>
|
||||
</>
|
||||
}
|
||||
renderOption={(props, option: DropdownOption) => (
|
||||
<li {...props} style={{whiteSpace: "normal", fontSize: fontSize, paddingLeft: `${optionPaddingLeftRems}rem`}}>{option.label}</li>
|
||||
)}
|
||||
|
||||
noOptionsText={<Box fontSize={fontSize}>No options found</Box>}
|
||||
|
||||
slotProps={{
|
||||
popper: {
|
||||
sx: {
|
||||
width: `${width}px!important`
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{customTimes}
|
||||
if (type == "DATE_PICKER")
|
||||
{
|
||||
return (
|
||||
<Box sx={{
|
||||
...sx,
|
||||
background: "white",
|
||||
width: "250px",
|
||||
borderRadius: "0.75rem !important",
|
||||
border: `1px solid ${colors.grayLines.main}`,
|
||||
"& *": {cursor: "pointer"}
|
||||
}} display="flex" alignItems="center" onClick={(event) => doForceOpen(event)}>
|
||||
{allowBackAndForth && <IconButton sx={{padding: 0, margin: "8px"}} onClick={(event) => navigateBackAndForth(event, backAndForthInverted ? 1 : -1, type)} disabled={backDisabled}><Icon>navigate_before</Icon></IconButton>}
|
||||
<LocalizationProvider dateAdapter={AdapterDayjs}>
|
||||
<DatePicker
|
||||
sx={{paddingRight: "8px"}}
|
||||
defaultValue={dayjs(defaultValue)}
|
||||
name={name}
|
||||
value={dayjs(dateValue)}
|
||||
onChange={handleDatePickerOnChange}
|
||||
slots={{
|
||||
openPickerIcon: CalendarTodayOutlined
|
||||
}}
|
||||
slotProps={{
|
||||
openPickerIcon: {sx: {fontSize: "1.25rem !important", color: "#757575"}},
|
||||
actionBar: {actions: ["today"]},
|
||||
textField: {variant: "standard", InputProps: {sx: {fontSize: "16px", color: "#495057"}, disableUnderline: true}}
|
||||
}}
|
||||
/>
|
||||
</LocalizationProvider>
|
||||
{allowBackAndForth && <IconButton onClick={(event) => navigateBackAndForth(event, backAndForthInverted ? -1 : 1, type)} disabled={forthDisabled}><Icon>navigate_next</Icon></IconButton>}
|
||||
</Box>
|
||||
) : null
|
||||
);
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
return (
|
||||
dropdownOptions ? (
|
||||
<Box sx={{
|
||||
whiteSpace: "nowrap", display: "flex",
|
||||
"& .MuiPopperUnstyled-root": {
|
||||
border: `1px solid ${colors.grayLines.main}`,
|
||||
borderTop: "none",
|
||||
borderRadius: "0 0 0.75rem 0.75rem",
|
||||
padding: 0,
|
||||
}, "& .MuiPaper-rounded": {
|
||||
borderRadius: "0 0 0.75rem 0.75rem",
|
||||
}
|
||||
}} className="dashboardDropdownMenu">
|
||||
<Autocomplete
|
||||
id={`${label}-combo-box`}
|
||||
|
||||
defaultValue={defaultValue}
|
||||
value={value}
|
||||
onChange={handleOnChange}
|
||||
inputValue={inputValue}
|
||||
onInputChange={handleOnInputChange}
|
||||
|
||||
isOptionEqualToValue={(option, value) => option.id === value.id}
|
||||
|
||||
open={isOpen}
|
||||
onOpen={() => setIsOpen(true)}
|
||||
onClose={() => setIsOpen(false)}
|
||||
|
||||
size="small"
|
||||
disablePortal
|
||||
disableClearable={disableClearable}
|
||||
options={dropdownOptions}
|
||||
sx={{
|
||||
...sx,
|
||||
cursor: "pointer",
|
||||
display: "inline-block",
|
||||
"& .MuiOutlinedInput-notchedOutline": {
|
||||
border: "none"
|
||||
},
|
||||
}}
|
||||
renderInput={(params: any) =>
|
||||
<>
|
||||
<Box sx={{width: `${width}px`, background: "white", borderRadius: isOpen ? "0.75rem 0.75rem 0 0" : "0.75rem", border: `1px solid ${colors.grayLines.main}`, "& *": {cursor: "pointer"}}} display="flex" alignItems="center" onClick={(event) => doForceOpen(event)}>
|
||||
{allowBackAndForth && <IconButton onClick={(event) => navigateBackAndForth(event, backAndForthInverted ? 1 : -1, type)} disabled={backDisabled}><Icon>navigate_before</Icon></IconButton>}
|
||||
<TextField {...params} placeholder={label} sx={{
|
||||
"& .MuiInputBase-input": {
|
||||
fontSize: fontSize
|
||||
}
|
||||
}} InputProps={{...params.InputProps, startAdornment: startAdornment/*, endAdornment: endAdornment*/}}
|
||||
/>
|
||||
{allowBackAndForth && <IconButton onClick={(event) => navigateBackAndForth(event, backAndForthInverted ? -1 : 1, type)} disabled={forthDisabled}><Icon>navigate_next</Icon></IconButton>}
|
||||
</Box>
|
||||
</>
|
||||
}
|
||||
renderOption={(props, option: DropdownOption) => (
|
||||
<li {...props} style={{whiteSpace: "normal", fontSize: fontSize, paddingLeft: `${optionPaddingLeftRems}rem`}}>{option.label}</li>
|
||||
)}
|
||||
|
||||
noOptionsText={<Box fontSize={fontSize}>No options found</Box>}
|
||||
|
||||
slotProps={{
|
||||
popper: {
|
||||
sx: {
|
||||
width: `${width}px!important`
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{customTimes}
|
||||
</Box>
|
||||
) : null
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default WidgetDropdownMenu;
|
||||
|
@ -21,8 +21,8 @@
|
||||
|
||||
import Box from "@mui/material/Box";
|
||||
import {Theme} from "@mui/material/styles";
|
||||
import {ReactNode} from "react";
|
||||
import colors from "qqq/assets/theme/base/colors";
|
||||
import {ReactNode} from "react";
|
||||
|
||||
// Declaring prop types for DataTableBodyCell
|
||||
interface Props
|
||||
@ -49,7 +49,7 @@ function DataTableBodyCell({noBorder, align, children}: Props): JSX.Element
|
||||
"@media (max-width: 1440px)": {
|
||||
fontSize: "0.875rem"
|
||||
},
|
||||
"&:nth-child(1)": {
|
||||
"&:nth-of-type(1)": {
|
||||
paddingLeft: "1rem"
|
||||
},
|
||||
"&:last-child": {
|
||||
|
@ -23,9 +23,9 @@ import Box from "@mui/material/Box";
|
||||
import Icon from "@mui/material/Icon";
|
||||
import {Theme} from "@mui/material/styles";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import {ReactNode} from "react";
|
||||
import colors from "qqq/assets/theme/base/colors";
|
||||
import {useMaterialUIController} from "qqq/context";
|
||||
import {ReactNode} from "react";
|
||||
|
||||
// Declaring props types for DataTableHeadCell
|
||||
interface Props
|
||||
@ -50,7 +50,7 @@ function DataTableHeadCell({width, children, sorted, align, tooltip, ...rest}: P
|
||||
px={1.5}
|
||||
sx={({palette: {light}, borders: {borderWidth}}: Theme) => ({
|
||||
borderBottom: `${borderWidth[1]} solid ${colors.grayLines.main}`,
|
||||
"&:nth-child(1)": {
|
||||
"&:nth-of-type(1)": {
|
||||
paddingLeft: "1rem"
|
||||
},
|
||||
"&:last-child": {
|
||||
|
@ -31,8 +31,6 @@ import Box from "@mui/material/Box";
|
||||
import Card from "@mui/material/Card";
|
||||
import Divider from "@mui/material/Divider";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import React, {useContext, useEffect, useState} from "react";
|
||||
import {Link, useLocation} from "react-router-dom";
|
||||
import QContext from "QContext";
|
||||
import colors from "qqq/assets/theme/base/colors";
|
||||
import MDTypography from "qqq/components/legacy/MDTypography";
|
||||
@ -41,6 +39,8 @@ import DashboardWidgets from "qqq/components/widgets/DashboardWidgets";
|
||||
import MiniStatisticsCard from "qqq/components/widgets/statistics/MiniStatisticsCard";
|
||||
import BaseLayout from "qqq/layouts/BaseLayout";
|
||||
import Client from "qqq/utils/qqq/Client";
|
||||
import React, {useContext, useEffect, useState} from "react";
|
||||
import {Link, useLocation} from "react-router-dom";
|
||||
|
||||
const qController = Client.getInstance();
|
||||
|
||||
@ -62,7 +62,7 @@ function AppHome({app}: Props): JSX.Element
|
||||
const [updatedTableCounts, setUpdatedTableCounts] = useState(new Date());
|
||||
const [widgets, setWidgets] = useState([] as any[]);
|
||||
|
||||
const {pageHeader, setPageHeader} = useContext(QContext);
|
||||
const {pageHeader, recordAnalytics, setPageHeader} = useContext(QContext);
|
||||
|
||||
const location = useLocation();
|
||||
|
||||
@ -86,8 +86,9 @@ function AppHome({app}: Props): JSX.Element
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
// setPageHeader(app.label);
|
||||
setPageHeader(null);
|
||||
recordAnalytics({location: window.location, title: "App: " + app.label});
|
||||
recordAnalytics({category: "appEvents", action: "loadAppScreen", label: app.label});
|
||||
|
||||
if (!qInstance)
|
||||
{
|
||||
|
@ -91,7 +91,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
const processNameParam = useParams().processName;
|
||||
const processName = process === null ? processNameParam : process.name;
|
||||
let tableVariantLocalStorageKey: string | null = null;
|
||||
if(table)
|
||||
if (table)
|
||||
{
|
||||
tableVariantLocalStorageKey = `${TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT}.${table.name}`;
|
||||
}
|
||||
@ -124,7 +124,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
const [showErrorDetail, setShowErrorDetail] = useState(false);
|
||||
const [showFullHelpText, setShowFullHelpText] = useState(false);
|
||||
|
||||
const {pageHeader, setPageHeader} = useContext(QContext);
|
||||
const {pageHeader, recordAnalytics, setPageHeader} = useContext(QContext);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// for setting the processError state - call this function, which will also set the isUserFacingError state //
|
||||
@ -427,10 +427,10 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
//////////////////////////////////////////////////
|
||||
step.components && (step.components.map((component: QFrontendComponent, index: number) =>
|
||||
{
|
||||
let helpRoles = ["PROCESS_SCREEN", "ALL_SCREENS"]
|
||||
if(component.type == QComponentType.BULK_EDIT_FORM)
|
||||
let helpRoles = ["PROCESS_SCREEN", "ALL_SCREENS"];
|
||||
if (component.type == QComponentType.BULK_EDIT_FORM)
|
||||
{
|
||||
helpRoles = ["EDIT_SCREEN", "WRITE_SCREENS", "ALL_SCREENS"]
|
||||
helpRoles = ["EDIT_SCREEN", "WRITE_SCREENS", "ALL_SCREENS"];
|
||||
}
|
||||
|
||||
return (
|
||||
@ -1157,6 +1157,10 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
const processMetaData = await Client.getInstance().loadProcessMetaData(processName);
|
||||
setProcessMetaData(processMetaData);
|
||||
setSteps(processMetaData.frontendSteps);
|
||||
|
||||
recordAnalytics({location: window.location, title: "Process: " + processMetaData?.label});
|
||||
recordAnalytics({category: "processEvents", action: "startProcess", label: processMetaData?.label});
|
||||
|
||||
if (processMetaData.tableName && !tableMetaData)
|
||||
{
|
||||
try
|
||||
@ -1262,6 +1266,8 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
|
||||
setTimeout(async () =>
|
||||
{
|
||||
recordAnalytics({category: "processEvents", action: "processStep", label: activeStep.label});
|
||||
|
||||
const processResponse = await Client.getInstance().processStep(
|
||||
processName,
|
||||
processUUID,
|
||||
|
@ -403,7 +403,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, initialQueryFilter, init
|
||||
/////////////////////////////
|
||||
// page context references //
|
||||
/////////////////////////////
|
||||
const {accentColor, accentColorLight, setPageHeader, dotMenuOpen, keyboardHelpOpen} = useContext(QContext);
|
||||
const {accentColor, accentColorLight, setPageHeader, recordAnalytics, dotMenuOpen, keyboardHelpOpen} = useContext(QContext);
|
||||
|
||||
//////////////////////////////////////////////////////////////////
|
||||
// we use our own header - so clear out the context page header //
|
||||
@ -630,25 +630,25 @@ const RecordQuery = forwardRef(({table, usage, isModal, initialQueryFilter, init
|
||||
|
||||
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();
|
||||
|
||||
@ -898,6 +898,8 @@ const RecordQuery = forwardRef(({table, usage, isModal, initialQueryFilter, init
|
||||
return;
|
||||
}
|
||||
|
||||
recordAnalytics({category: "tableEvents", action: "query", label: tableMetaData.label});
|
||||
|
||||
console.log(`In updateTable for ${reason} ${JSON.stringify(queryFilter)}`);
|
||||
setLoading(true);
|
||||
setRows([]);
|
||||
@ -1671,6 +1673,8 @@ const RecordQuery = forwardRef(({table, usage, isModal, initialQueryFilter, init
|
||||
{
|
||||
if (selectedSavedViewId != null)
|
||||
{
|
||||
recordAnalytics({category: "tableEvents", action: "activateSavedView", label: tableMetaData.label});
|
||||
|
||||
//////////////////////////////////////////////
|
||||
// fetch, then activate the selected filter //
|
||||
//////////////////////////////////////////////
|
||||
@ -1686,6 +1690,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, initialQueryFilter, init
|
||||
/////////////////////////////////
|
||||
// this is 'new view' - right? //
|
||||
/////////////////////////////////
|
||||
recordAnalytics({category: "tableEvents", action: "activateNewView", label: tableMetaData.label});
|
||||
|
||||
//////////////////////////////
|
||||
// wipe away the saved view //
|
||||
@ -2370,6 +2375,8 @@ const RecordQuery = forwardRef(({table, usage, isModal, initialQueryFilter, init
|
||||
setTableMetaData(tableMetaData);
|
||||
setTableLabel(tableMetaData.label);
|
||||
|
||||
recordAnalytics({location: window.location, title: "Query: " + tableMetaData.label});
|
||||
|
||||
setTableProcesses(ProcessUtils.getProcessesForTable(metaData, tableName)); // these are the ones to show in the dropdown
|
||||
setAllTableProcesses(ProcessUtils.getProcessesForTable(metaData, tableName, true)); // these include hidden ones (e.g., to find the bulks)
|
||||
|
||||
|
@ -28,9 +28,6 @@ import Button from "@mui/material/Button";
|
||||
import Card from "@mui/material/Card";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import Snackbar from "@mui/material/Snackbar";
|
||||
import React, {useContext, useReducer, useState} from "react";
|
||||
import AceEditor from "react-ace";
|
||||
import {useParams} from "react-router-dom";
|
||||
import QContext from "QContext";
|
||||
import ScriptViewer from "qqq/components/widgets/misc/ScriptViewer";
|
||||
import BaseLayout from "qqq/layouts/BaseLayout";
|
||||
@ -41,6 +38,9 @@ import "ace-builds/src-noconflict/mode-java";
|
||||
import "ace-builds/src-noconflict/mode-javascript";
|
||||
import "ace-builds/src-noconflict/mode-json";
|
||||
import "ace-builds/src-noconflict/theme-github";
|
||||
import React, {useContext, useReducer, useState} from "react";
|
||||
import AceEditor from "react-ace";
|
||||
import {useParams} from "react-router-dom";
|
||||
import "ace-builds/src-noconflict/ext-language_tools";
|
||||
|
||||
const qController = Client.getInstance();
|
||||
@ -69,13 +69,9 @@ function RecordDeveloperView({table}: Props): JSX.Element
|
||||
const [associatedScripts, setAssociatedScripts] = useState([] as any[]);
|
||||
const [notFoundMessage, setNotFoundMessage] = useState(null);
|
||||
|
||||
const [selectedTabs, setSelectedTabs] = useState({} as any);
|
||||
const [viewingRevisions, setViewingRevisions] = useState({} as any);
|
||||
const [scriptLogs, setScriptLogs] = useState({} as any);
|
||||
|
||||
const [alertText, setAlertText] = useState(null as string);
|
||||
|
||||
const {setPageHeader} = useContext(QContext);
|
||||
const {setPageHeader, recordAnalytics} = useContext(QContext);
|
||||
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
||||
|
||||
if (!asyncLoadInited)
|
||||
@ -90,6 +86,8 @@ function RecordDeveloperView({table}: Props): JSX.Element
|
||||
const tableMetaData = await qController.loadTableMetaData(tableName);
|
||||
setTableMetaData(tableMetaData);
|
||||
|
||||
recordAnalytics({location: window.location, title: "Developer Mode: " + tableMetaData.label});
|
||||
|
||||
//////////////////////////////
|
||||
// load top-level meta-data //
|
||||
//////////////////////////////
|
||||
|
@ -121,7 +121,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
||||
const openActionsMenu = (event: any) => setActionsMenu(event.currentTarget);
|
||||
const closeActionsMenu = () => setActionsMenu(null);
|
||||
|
||||
const {accentColor, setPageHeader, tableMetaData, setTableMetaData, tableProcesses, setTableProcesses, dotMenuOpen, keyboardHelpOpen, helpHelpActive} = useContext(QContext);
|
||||
const {accentColor, setPageHeader, tableMetaData, setTableMetaData, tableProcesses, setTableProcesses, dotMenuOpen, keyboardHelpOpen, helpHelpActive, recordAnalytics} = useContext(QContext);
|
||||
|
||||
if (localStorage.getItem(tableVariantLocalStorageKey))
|
||||
{
|
||||
@ -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");
|
||||
@ -384,6 +384,8 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
||||
const tableMetaData = await qController.loadTableMetaData(tableName);
|
||||
setTableMetaData(tableMetaData);
|
||||
|
||||
recordAnalytics({location: window.location, title: "View: " + tableMetaData.label});
|
||||
|
||||
//////////////////////////////////////////////////////////////////
|
||||
// load top-level meta-data (e.g., to find processes for table) //
|
||||
//////////////////////////////////////////////////////////////////
|
||||
@ -430,6 +432,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
||||
{
|
||||
record = await qController.get(tableName, id, tableVariant, null, queryJoins);
|
||||
setRecord(record);
|
||||
recordAnalytics({category: "tableEvents", action: "view", label: tableMetaData?.label + " / " + record?.recordLabel});
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
@ -631,6 +634,8 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
||||
event?.preventDefault();
|
||||
(async () =>
|
||||
{
|
||||
recordAnalytics({category: "tableEvents", action: "delete", label: tableMetaData?.label + " / " + record?.recordLabel});
|
||||
|
||||
await qController.delete(tableName, id)
|
||||
.then(() =>
|
||||
{
|
||||
|
@ -158,6 +158,7 @@ but we've turned off the click-to-sort function, so remove hand cursor */
|
||||
white-space: normal;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.MuiDataGrid-filterForm
|
||||
{
|
||||
align-items: flex-end;
|
||||
@ -173,10 +174,12 @@ but we've turned off the click-to-sort function, so remove hand cursor */
|
||||
{
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.MuiDataGrid-filterForm .MuiDataGrid-filterFormValueInput
|
||||
{
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.MuiDataGrid-filterForm .MuiDataGrid-filterFormOperatorInput
|
||||
{
|
||||
width: 150px;
|
||||
@ -187,13 +190,14 @@ but we've turned off the click-to-sort function, so remove hand cursor */
|
||||
{
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.MuiDataGrid-filterForm .MuiDataGrid-filterFormValueInput .MuiAutocomplete-root .MuiAutocomplete-endAdornment svg
|
||||
{
|
||||
height: 0.625em;
|
||||
}
|
||||
|
||||
/* fix strange size bug on filter autocompletes */
|
||||
.MuiDataGrid-filterForm .MuiDataGrid-filterFormValueInput>.MuiBox-root>.MuiBox-root:has(>.MuiAutocomplete-root)
|
||||
.MuiDataGrid-filterForm .MuiDataGrid-filterFormValueInput > .MuiBox-root > .MuiBox-root:has(>.MuiAutocomplete-root)
|
||||
{
|
||||
margin-bottom: 0;
|
||||
width: 100%;
|
||||
@ -208,16 +212,31 @@ but we've turned off the click-to-sort function, so remove hand cursor */
|
||||
}
|
||||
|
||||
/* clears the ‘X’ from Internet Explorer */
|
||||
input[type=search]::-ms-clear { display: none; width : 0; height: 0; }
|
||||
input[type=search]::-ms-reveal { display: none; width : 0; height: 0; }
|
||||
input[type=search]::-ms-clear
|
||||
{
|
||||
display: none;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
input[type=search]::-ms-reveal
|
||||
{
|
||||
display: none;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/* clears the ‘X’ from Chrome */
|
||||
input[type="search"]::-webkit-search-decoration,
|
||||
input[type="search"]::-webkit-search-cancel-button,
|
||||
input[type="search"]::-webkit-search-results-button,
|
||||
input[type="search"]::-webkit-search-results-decoration { display: none; }
|
||||
input[type="search"]::-webkit-search-results-decoration
|
||||
{
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Shrink the big margin-bottom on modal processes */
|
||||
.modalProcess>.MuiBox-root>.MuiBox-root
|
||||
.modalProcess > .MuiBox-root > .MuiBox-root
|
||||
{
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
@ -270,6 +289,7 @@ input[type="search"]::-webkit-search-results-decoration { display: none; }
|
||||
color: initial !important;
|
||||
border: 1px solid rgb(206, 212, 218);
|
||||
}
|
||||
|
||||
.MuiDataGrid-filterForm .MuiAutocomplete-tag .MuiSvgIcon-root
|
||||
{
|
||||
color: initial !important;
|
||||
@ -287,7 +307,7 @@ input[type="search"]::-webkit-search-results-decoration { display: none; }
|
||||
right: 0.125rem;
|
||||
}
|
||||
|
||||
.devDocumentation ul>li
|
||||
.devDocumentation ul > li
|
||||
{
|
||||
margin-left: 30px;
|
||||
}
|
||||
@ -640,6 +660,7 @@ input[type="search"]::-webkit-search-results-decoration { display: none; }
|
||||
border: 1px solid #BDBDBD;
|
||||
border-radius: 0.5rem !important;
|
||||
}
|
||||
|
||||
.MuiToggleButtonGroup-root .MuiButtonBase-root
|
||||
{
|
||||
text-transform: none;
|
||||
@ -650,10 +671,12 @@ input[type="search"]::-webkit-search-results-decoration { display: none; }
|
||||
border: none;
|
||||
flex: 1 1 0px;
|
||||
}
|
||||
|
||||
.MuiToggleButtonGroup-root .MuiButtonBase-root.Mui-selected
|
||||
{
|
||||
background: rgba(117, 117, 117, 0.20);
|
||||
}
|
||||
|
||||
.MuiToggleButtonGroup-root .MuiButtonBase-root.Mui-disabled
|
||||
{
|
||||
border: none;
|
||||
@ -663,4 +686,10 @@ input[type="search"]::-webkit-search-results-decoration { display: none; }
|
||||
.recordView h5
|
||||
{
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.MuiPickersDay-root.Mui-selected, .MuiPickersDay-root.MuiPickersDay-dayWithMargin:hover
|
||||
{
|
||||
color: white;
|
||||
background-color: #0062FF !important;
|
||||
}
|
||||
|
143
src/qqq/utils/GoogleAnalyticsUtils.ts
Normal file
143
src/qqq/utils/GoogleAnalyticsUtils.ts
Normal file
@ -0,0 +1,143 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
|
||||
import Client from "qqq/utils/qqq/Client";
|
||||
import ReactGA from "react-ga4";
|
||||
|
||||
|
||||
export interface PageView
|
||||
{
|
||||
location: Location;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface UserEvent
|
||||
{
|
||||
action: string;
|
||||
category: string;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export type AnalyticsModel = PageView | UserEvent;
|
||||
|
||||
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 = (model: AnalyticsModel) =>
|
||||
{
|
||||
if(!this.active)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if(model.hasOwnProperty("location"))
|
||||
{
|
||||
const pageView = model as PageView;
|
||||
ReactGA.send({hitType: "pageview", page: pageView.location.pathname + pageView.location.search, title: pageView.title});
|
||||
}
|
||||
else if(model.hasOwnProperty("action") || model.hasOwnProperty("category") || model.hasOwnProperty("label"))
|
||||
{
|
||||
const userEvent = model as UserEvent;
|
||||
ReactGA.event({action: userEvent.action, category: userEvent.category, label: userEvent.label})
|
||||
}
|
||||
else
|
||||
{
|
||||
console.log("Unrecognizable analytics model", model);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private setup = async (): Promise<void> =>
|
||||
{
|
||||
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 = (model: AnalyticsModel) =>
|
||||
{
|
||||
if(this.metaData == null)
|
||||
{
|
||||
(async () =>
|
||||
{
|
||||
await this.setup();
|
||||
})()
|
||||
}
|
||||
|
||||
this.send(model);
|
||||
}
|
||||
|
||||
}
|
@ -35,7 +35,7 @@ class Client
|
||||
{
|
||||
console.log(`Caught Exception: ${JSON.stringify(exception)}`);
|
||||
|
||||
if(exception && exception.status == 401 && Client.unauthorizedCallback)
|
||||
if (exception && exception.status == 401 && Client.unauthorizedCallback)
|
||||
{
|
||||
console.log("This is a 401 - calling the unauthorized callback.");
|
||||
Client.unauthorizedCallback();
|
||||
|
@ -219,6 +219,16 @@ class ValueUtils
|
||||
|
||||
if (field.type === QFieldType.DATE_TIME)
|
||||
{
|
||||
if(displayValue && displayValue != rawValue)
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// if the date-time actually has a displayValue set, and it isn't just the //
|
||||
// raw-value being copied into the display value by whoever called us, then //
|
||||
// return the display value. //
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
return displayValue;
|
||||
}
|
||||
|
||||
if (!rawValue)
|
||||
{
|
||||
return ("");
|
||||
@ -270,6 +280,7 @@ class ValueUtils
|
||||
{
|
||||
date = new Date(date);
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
return (`${date.toString("yyyy-MM-dd hh:mm:ss")} ${date.getHours() < 12 ? "AM" : "PM"} ${date.getTimezone()}`);
|
||||
}
|
||||
|
Reference in New Issue
Block a user