diff --git a/src/qqq/components/databags/DataBagDataEditor.tsx b/src/qqq/components/databags/DataBagDataEditor.tsx
new file mode 100644
index 0000000..8b7b115
--- /dev/null
+++ b/src/qqq/components/databags/DataBagDataEditor.tsx
@@ -0,0 +1,209 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * 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/
+ *
+ * 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 {QJobError} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobError";
+import {ToggleButton, ToggleButtonGroup, Typography} from "@mui/material";
+import Alert from "@mui/material/Alert";
+import Box from "@mui/material/Box";
+import Card from "@mui/material/Card";
+import Grid from "@mui/material/Grid";
+import Snackbar from "@mui/material/Snackbar";
+import TextField from "@mui/material/TextField";
+import FormData from "form-data";
+import React, {useReducer, useState} from "react";
+import AceEditor from "react-ace";
+import {QCancelButton, QSaveButton} from "qqq/components/buttons/DefaultButtons";
+import Client from "qqq/utils/qqq/Client";
+import DataBagPreview from "./DataBagPreview";
+
+export interface DataBagDataEditorProps
+{
+ title: string;
+ dataBagId: number;
+ data: string;
+ closeCallback: any;
+}
+
+
+const qController = Client.getInstance();
+
+function DataBagDataEditor({title, dataBagId, data, closeCallback}: DataBagDataEditorProps): JSX.Element
+{
+ const [closing, setClosing] = useState(false);
+ const [updatedCode, setUpdatedCode] = useState(data)
+ const [commitMessage, setCommitMessage] = useState("")
+ const [openTool, setOpenTool] = useState(null);
+ const [errorAlert, setErrorAlert] = useState("")
+ const [, forceUpdate] = useReducer((x) => x + 1, 0);
+
+ const changeOpenTool = (event: React.MouseEvent, newValue: string | null) =>
+ {
+ setOpenTool(newValue);
+
+ // need this to make Ace recognize new height.
+ setTimeout(() =>
+ {
+ window.dispatchEvent(new Event("resize"))
+ }, 100);
+ };
+
+ const saveClicked = () =>
+ {
+ try
+ {
+ JSON.parse(updatedCode)
+ }
+ catch(e)
+ {
+ setErrorAlert("Cannot save Data Bag Contents. Invalid json: " + e);
+ return;
+ }
+
+ setClosing(true);
+
+ (async () =>
+ {
+ const formData = new FormData();
+ formData.append("dataBagId", dataBagId);
+ formData.append("data", updatedCode);
+ formData.append("commitMessage", commitMessage);
+
+ //////////////////////////////////////////////////////////////////
+ // we don't want this job to go async, so, pass a large timeout //
+ //////////////////////////////////////////////////////////////////
+ formData.append("_qStepTimeoutMillis", 60 * 1000);
+
+ const formDataHeaders = {
+ "content-type": "multipart/form-data; boundary=--------------------------320289315924586491558366",
+ };
+
+ const processResult = await qController.processInit("storeDataBagVersion", formData, formDataHeaders);
+ if (processResult instanceof QJobError)
+ {
+ const jobError = processResult as QJobError
+ closeCallback(null, "failed", jobError.userFacingError ?? jobError.error);
+ }
+ console.log("process result");
+ console.log(processResult);
+
+ closeCallback(null, "saved", "Saved New Data Bag Version");
+ })();
+ }
+
+ const cancelClicked = () =>
+ {
+ setClosing(true);
+ closeCallback(null, "cancelled");
+ }
+
+ const updateCode = (value: string, event: any) =>
+ {
+ console.log("Updating code")
+ setUpdatedCode(value);
+ forceUpdate();
+ }
+
+ const updateCommitMessage = (event: React.ChangeEvent) =>
+ {
+ setCommitMessage(event.target.value);
+ }
+
+ return (
+
+
+
+
+ {
+ if (reason === "clickaway")
+ {
+ return;
+ }
+ setErrorAlert("")
+ }} anchorOrigin={{vertical: "top", horizontal: "center"}}>
+ setErrorAlert("")}>
+ {errorAlert}
+
+
+
+
+
+ {title}
+
+
+
+
+ Tools:
+
+
+ Preview
+
+
+
+
+
+
+
+
+ {
+ openTool &&
+
+ {
+ openTool == "preview" &&
+
+
+
+ }
+
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default DataBagDataEditor;
diff --git a/src/qqq/components/databags/DataBagPreview.tsx b/src/qqq/components/databags/DataBagPreview.tsx
new file mode 100644
index 0000000..e0f3a04
--- /dev/null
+++ b/src/qqq/components/databags/DataBagPreview.tsx
@@ -0,0 +1,141 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2023. 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 Box from "@mui/material/Box";
+import Icon from "@mui/material/Icon";
+import React, {useEffect, useReducer, useState} from "react";
+
+interface Props
+{
+ data?: any;
+ json?: string;
+}
+
+DataBagPreview.defaultProps = {
+ data: null,
+ json: null,
+};
+
+function DataBagPreview({data, json}: Props): JSX.Element
+{
+ const [openPreviewDivs, setOpenPreviewDivs] = useState(new Set)
+ const [, forceUpdate] = useReducer((x) => x + 1, 0);
+ const [dataToRender, setDataToRender] = useState(null as any);
+ const [errorMessage, setErrorMessage] = useState(null as string)
+
+ useEffect(() =>
+ {
+ if(data)
+ {
+ setDataToRender(data);
+ }
+ else
+ {
+ try
+ {
+ const dataToRender = JSON.parse(json)
+ setDataToRender(dataToRender);
+ setErrorMessage(null);
+ }
+ catch(e)
+ {
+ setErrorMessage("Error parsing JSON: " + e);
+ }
+ }
+ }, [data, json]);
+
+ const togglePreviewDiv = (id: string) =>
+ {
+ if(openPreviewDivs.has(id))
+ {
+ openPreviewDivs.delete(id);
+ }
+ else
+ {
+ openPreviewDivs.add(id);
+ }
+ setOpenPreviewDivs(openPreviewDivs);
+ console.log(openPreviewDivs);
+ forceUpdate();
+ }
+
+ const previewObject = (object: any, path: string): JSX.Element =>
+ {
+ if(typeof object == "object")
+ {
+ return (
+ <>
+
+ {
+ Object.keys(object).map((key: any, index: any) =>
+ {
+ const divId = `${path}.${key}`
+ const childIsObject = (typeof object[key] == "object");
+ return (
+
+
+ {
+ childIsObject
+ ? togglePreviewDiv(divId)} style={{cursor: "pointer"}}>{openPreviewDivs.has(divId) ? "expand_more" : "chevron_right"}
+ : •
+ }
+ {
+ childIsObject
+ ? togglePreviewDiv(divId)} style={{"cursor": "pointer"}}>{key}:
+ : {key}:
+ }
+ {
+ childIsObject && openPreviewDivs.has(divId) && {previewObject(object[key], `${path}.${key}`)}
+ }
+ {
+ !childIsObject && {object[key]}
+ }
+
+
+ );
+ })
+ }
+
+ >
+ )
+ }
+ else
+ {
+ return (<>{object}>)
+ }
+ }
+
+ const getDataBagPreview = (data: any): JSX.Element=>
+ {
+ console.log("getDataBagPreview:")
+ return previewObject(data, "");
+ }
+
+ return (
+ <>
+ {errorMessage == null && dataToRender && getDataBagPreview(dataToRender)}
+ {errorMessage && {errorMessage}}
+ >
+ )
+}
+
+export default DataBagPreview;
diff --git a/src/qqq/components/widgets/DashboardWidgets.tsx b/src/qqq/components/widgets/DashboardWidgets.tsx
index b64e2e8..df3d149 100644
--- a/src/qqq/components/widgets/DashboardWidgets.tsx
+++ b/src/qqq/components/widgets/DashboardWidgets.tsx
@@ -36,6 +36,7 @@ import DefaultLineChart from "qqq/components/widgets/charts/linechart/DefaultLin
import SmallLineChart from "qqq/components/widgets/charts/linechart/SmallLineChart";
import PieChart from "qqq/components/widgets/charts/piechart/PieChart";
import StackedBarChart from "qqq/components/widgets/charts/StackedBarChart";
+import DataBagViewer from "qqq/components/widgets/misc/DataBagViewer";
import DividerWidget from "qqq/components/widgets/misc/Divider";
import FieldValueListWidget from "qqq/components/widgets/misc/FieldValueListWidget";
import QuickSightChart from "qqq/components/widgets/misc/QuickSightChart";
@@ -419,6 +420,14 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
/>
)
}
+ {
+ widgetMetaData.type === "dataBagViewer" && (
+ widgetData && widgetData[i] && widgetData[i].queryParams &&
+
+
+
+ )
+ }
);
}
diff --git a/src/qqq/components/widgets/misc/DataBagViewer.tsx b/src/qqq/components/widgets/misc/DataBagViewer.tsx
new file mode 100644
index 0000000..cdcc604
--- /dev/null
+++ b/src/qqq/components/widgets/misc/DataBagViewer.tsx
@@ -0,0 +1,393 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * 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/
+ *
+ * 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 {QException} from "@kingsrook/qqq-frontend-core/lib/exceptions/QException";
+import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
+import {QCriteriaOperator} from "@kingsrook/qqq-frontend-core/lib/model/query/QCriteriaOperator";
+import {QFilterCriteria} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterCriteria";
+import {QFilterOrderBy} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterOrderBy";
+import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
+import Alert from "@mui/material/Alert";
+import Avatar from "@mui/material/Avatar";
+import Box from "@mui/material/Box";
+import Button from "@mui/material/Button";
+import Chip from "@mui/material/Chip";
+import Divider from "@mui/material/Divider";
+import Grid from "@mui/material/Grid";
+import Icon from "@mui/material/Icon";
+import List from "@mui/material/List";
+import ListItem from "@mui/material/ListItem";
+import ListItemAvatar from "@mui/material/ListItemAvatar";
+import ListItemText from "@mui/material/ListItemText";
+import Modal from "@mui/material/Modal";
+import Snackbar from "@mui/material/Snackbar";
+import Tab from "@mui/material/Tab";
+import Tabs from "@mui/material/Tabs";
+import Typography from "@mui/material/Typography";
+import React, {useReducer, useState} from "react";
+import AceEditor from "react-ace";
+import DataBagDataEditor, {DataBagDataEditorProps} from "qqq/components/databags/DataBagDataEditor";
+import DataBagPreview from "qqq/components/databags/DataBagPreview";
+import TabPanel from "qqq/components/misc/TabPanel";
+import CustomWidthTooltip from "qqq/components/tooltips/CustomWidthTooltip";
+import {LoadingState} from "qqq/models/LoadingState";
+import DeveloperModeUtils from "qqq/utils/DeveloperModeUtils";
+import Client from "qqq/utils/qqq/Client";
+import ValueUtils from "qqq/utils/qqq/ValueUtils";
+
+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 "ace-builds/src-noconflict/ext-language_tools";
+
+const qController = Client.getInstance();
+
+// Declaring props types for ViewForm
+interface Props
+{
+ dataBagId: number
+}
+
+DataBagViewer.defaultProps =
+ {
+ };
+
+export default function DataBagViewer({dataBagId}: Props): JSX.Element
+{
+ const [dataBagRecord, setDataBagRecord] = useState(null as QRecord);
+ const [asyncLoadInited, setAsyncLoadInited] = useState(false);
+ const [versionRecordList, setVersionRecordList] = useState(null as QRecord[]);
+ const [selectedVersionRecord, setSelectedVersionRecord] = useState(null as QRecord);
+ const [currentVersionId , setCurrentVersionId] = useState(null as number);
+ const [notFoundMessage, setNotFoundMessage] = useState(null);
+ const [selectedTab, setSelectedTab] = useState(0);
+ const [editorProps, setEditorProps] = useState(null as DataBagDataEditorProps);
+ const [successText, setSuccessText] = useState(null as string);
+ const [failText, setFailText] = useState(null as string)
+ const [, forceUpdate] = useReducer((x) => x + 1, 0);
+
+ const [loadingSelectedVersion, _] = useState(new LoadingState(forceUpdate, "loading"));
+
+ if (!asyncLoadInited)
+ {
+ setAsyncLoadInited(true);
+
+ (async () =>
+ {
+ try
+ {
+ const dataBagRecord = await qController.get("dataBag", dataBagId);
+ setDataBagRecord(dataBagRecord);
+
+ const criteria = [new QFilterCriteria("dataBagId", QCriteriaOperator.EQUALS, [dataBagId])];
+ const orderBys = [new QFilterOrderBy("sequenceNo", false)];
+ const filter = new QQueryFilter(criteria, orderBys);
+ const versions = await qController.query("dataBagVersion", filter, 25, 0);
+ console.log("Fetched versions:");
+ console.log(versions);
+ setVersionRecordList(versions);
+
+ if(versions && versions.length > 0)
+ {
+ setCurrentVersionId(versions[0].values.get("id"));
+ const latestVersion = await qController.get("dataBagVersion", versions[0].values.get("id"));
+ console.log("Fetched latestVersion:");
+ console.log(latestVersion);
+ setSelectedVersionRecord(latestVersion);
+ loadingSelectedVersion.setNotLoading();
+ forceUpdate();
+ }
+ }
+ catch (e)
+ {
+ if (e instanceof QException)
+ {
+ if ((e as QException).status === "404")
+ {
+ setNotFoundMessage("Data bag data could not be found.");
+ return;
+ }
+ }
+ setNotFoundMessage("Error loading data bag data: " + e);
+ }
+ })();
+ }
+
+ const editData = (data: string) =>
+ {
+ const editorProps = {} as DataBagDataEditorProps;
+ editorProps.title = (data ? "Editing Contents of Data Bag: " : "Initializing Contents of Data Bag: ") + dataBagRecord?.values?.get("name");
+ editorProps.data = data;
+ editorProps.dataBagId = dataBagId;
+ setEditorProps(editorProps);
+ };
+
+ const closeEditingScript = (event: object, reason: string, alert: string = null) =>
+ {
+ if (reason === "backdropClick" || reason === "escapeKeyDown")
+ {
+ return;
+ }
+
+ if (reason === "saved")
+ {
+ setAsyncLoadInited(false);
+ forceUpdate();
+
+ if (alert)
+ {
+ setSuccessText(alert);
+ }
+ }
+ else if (reason === "failed")
+ {
+ setAsyncLoadInited(false);
+ forceUpdate();
+
+ if (alert)
+ {
+ setFailText(alert);
+ }
+ }
+
+ setEditorProps(null);
+ };
+
+ const changeTab = (newValue: number) =>
+ {
+ setSelectedTab(newValue);
+ forceUpdate();
+ };
+
+ const selectVersion = (version: QRecord) =>
+ {
+ (async () =>
+ {
+ // fetch the full version
+ setSelectedVersionRecord(version);
+ loadingSelectedVersion.setLoading();
+
+ const selectedVersion = await qController.get("dataBagVersion", version.values.get("id"));
+ console.log("Fetched selectedVersion:");
+ console.log(selectedVersion);
+ setSelectedVersionRecord(selectedVersion);
+ loadingSelectedVersion.setNotLoading();
+ forceUpdate();
+ })();
+ };
+
+ function getVersionsList(versionRecordList: QRecord[], selectedVersionRecord: QRecord)
+ {
+ return
+ {
+ (versionRecordList == null || versionRecordList.length == 0) ?
+
+ There are not any versions of this data bag.
+
+ : <>>
+ }
+ {
+ versionRecordList?.map((version: any) => (
+
+ selectVersion(version)}>
+
+ {`${version.values.get("sequenceNo")}`}
+
+
+ {currentVersionId == version?.values?.get("id") && }
+ {version.values.get("commitMessage")}
+
+ }
+ secondary={
+ <>
+ {ValueUtils.formatDateTime(version.values.get("createDate"))}
+
+ {version.values.get("author")}
+ >
+ }
+ />
+
+
+
+ ))
+ }
+
;
+ }
+
+ let editButtonTooltip = "";
+ let editButtonText = "Create New Version";
+ if (currentVersionId)
+ {
+ if (currentVersionId === selectedVersionRecord?.values?.get("id"))
+ {
+ editButtonTooltip = "If you make any changes to this data bag, a new version will be created when you hit Save.";
+ editButtonText = "Edit";
+ }
+ else
+ {
+ editButtonTooltip = "If you want to make this previous Version active, bring up the Edit window, make any changes " +
+ "to the old Version if they are needed, then click Save. A new Version will be created, and set as Current.";
+ editButtonText = "Edit and Activate";
+ }
+ }
+
+ return (
+
+
+
+ {
+ notFoundMessage
+ ?
+ {notFoundMessage}
+ :
+
+ {
+ successText ? (
+ setSuccessText(null)} anchorOrigin={{vertical: "top", horizontal: "center"}}>
+ setSuccessText(null)}>
+ {successText}
+
+
+ ) : ("")
+ }
+ {
+ failText ? (
+ setFailText(null)} anchorOrigin={{vertical: "top", horizontal: "center"}}>
+ setFailText(null)}>
+ {failText}
+
+
+ ) : ("")
+ }
+
+
+
+ <>
+
+
+ changeTab(newValue)}
+ variant="standard"
+ >
+
+
+
+
+
+
+
+
+
+ Versions
+
+ {getVersionsList(versionRecordList, selectedVersionRecord)}
+
+
+
+
+ {
+ selectedVersionRecord ?
+
+ Version {selectedVersionRecord.values.get("sequenceNo")}
+ {
+ currentVersionId === selectedVersionRecord.values.get("id")
+ ? (<> (Current)>)
+ : <>>
+ }
+
+ : <>>
+ }
+
+
+
+
+ {
+ loadingSelectedVersion.isNotLoading() && selectedVersionRecord && selectedVersionRecord.values.get("data") ? (
+ <>
+
+ >
+ ) : null
+ }
+ {
+ loadingSelectedVersion.isLoadingSlow() && Loading...
+ }
+
+
+
+
+
+
+
+ Versions
+
+ {getVersionsList(versionRecordList, selectedVersionRecord)}
+
+
+
+ Data Preview (Version {selectedVersionRecord?.values?.get("sequenceNo")})
+
+
+ {loadingSelectedVersion.isNotLoading() && selectedTab == 1 && selectedVersionRecord?.values?.get("data") && }
+ {loadingSelectedVersion.isLoadingSlow() && Loading...}
+
+
+
+
+ >
+
+
+
+ {
+ editorProps &&
+ closeEditingScript(event, reason)}>
+
+
+ }
+
+
+ }
+
+
+
+ );
+}
diff --git a/src/qqq/models/LoadingState.ts b/src/qqq/models/LoadingState.ts
new file mode 100644
index 0000000..39d7d27
--- /dev/null
+++ b/src/qqq/models/LoadingState.ts
@@ -0,0 +1,88 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2023. 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 .
+ */
+
+
+/*******************************************************************************
+ ** Object to represent when data is loading, e.g., from backend, and when you
+ ** want to be able to show an empty state at first, but then after a bit, switch
+ ** to a "loading..." type UI (the idea being, to not immediately show that "loading"
+ ** UI, for cases where that would cause maybe an undesirable flickering.
+ **
+ ** To use:
+ ** - Add 1 or more LoadingState objects as state variables in your component.
+ ** Note that you don't generally need to ever "set" these states.
+ ** Provide a "useReducer"/"forceUpdate" function to the constructor, and the initial state (default is "notLoading"), e.g.:
+ ** const [loadingSelectedVersion, _] = useState(new LoadingState(forceUpdate, "loading"));
+ ** - Before an `await`, call `.setLoading()` on your LoadingState instance.
+ ** Internally, that call will set a timeout that will switch to the loadingSlow state.
+ ** - After the `await`, call `.setNotLoading()` on your LoadingState instance.
+ ** Internally, that call will cancel the timeout that would have put us in loadingSlow.
+ ** (Assume you'll also have set some other state based on the data that came back from the await,
+ ** and that will trigger the next render - or - do you have make a forceUpdate() call there anyway?)
+ ** - In your template, before your "loaded" view, check for `myLoadingState.isNotLoading()`, e.g.
+ ** {myLoadingState.isNotLoading() && myData && ...
+ ** - In your template, before your "slow loading" view, check for `myLoadingState.isLoadingSlow()`, e.g.
+ ** {myLoadingState.isLoadingSlow() && }
+ *******************************************************************************/
+export class LoadingState
+{
+ private state: "notLoading" | "loading" | "slow"
+ private slowTimeout: any;
+ private forceUpdate: () => void
+
+ constructor(forceUpdate: () => void, initialState: "notLoading" | "loading" | "slow" = "notLoading")
+ {
+ this.forceUpdate = forceUpdate;
+ this.state = initialState;
+ }
+
+ public setLoading()
+ {
+ this.state = "loading";
+ this.slowTimeout = setTimeout(() =>
+ {
+ this.state = "slow";
+ this.forceUpdate();
+ }, 1000);
+ }
+
+ public setNotLoading()
+ {
+ clearTimeout(this.slowTimeout);
+ this.state = "notLoading";
+ }
+
+ public isLoading(): boolean
+ {
+ return (this.state == "loading");
+ }
+
+ public isLoadingSlow(): boolean
+ {
+ return (this.state == "slow");
+ }
+
+ public isNotLoading(): boolean
+ {
+ return (this.state == "notLoading");
+ }
+
+}
\ No newline at end of file
diff --git a/src/qqq/styles/qqq-override-styles.css b/src/qqq/styles/qqq-override-styles.css
index 590a38b..26f903c 100644
--- a/src/qqq/styles/qqq-override-styles.css
+++ b/src/qqq/styles/qqq-override-styles.css
@@ -310,4 +310,17 @@ input[type="search"]::-webkit-search-results-decoration { display: none; }
z-index: 1;
opacity: 1;
border-right: 0.0625rem solid rgb(222, 226, 230);
+}
+
+.fieldLabel
+{
+ color: rgb(52, 71, 103);
+ font-weight: 700;
+ padding-right: .5em;
+}
+
+.fieldValue
+{
+ color: rgb(123, 128, 154);
+ font-weight: 400;
}
\ No newline at end of file
diff --git a/src/qqq/utils/DeveloperModeUtils.tsx b/src/qqq/utils/DeveloperModeUtils.tsx
index e8f25b8..cbb1534 100644
--- a/src/qqq/utils/DeveloperModeUtils.tsx
+++ b/src/qqq/utils/DeveloperModeUtils.tsx
@@ -23,7 +23,7 @@
export default class DeveloperModeUtils
{
- public static revToColor = (fieldName: string, recordId: string, rev: number): string =>
+ public static revToColor = (fieldName: string, recordId: string | number, rev: number): string =>
{
let hash = 0;
let idFactor = 1;