reloadWidget(i, data)}
isChild={areChildren}
+ labelAdditionalComponentsRight={labelAdditionalComponentsRight}
+ labelAdditionalComponentsLeft={labelAdditionalComponentsLeft}
>
)
}
+ {
+ widgetMetaData.type === "composite" && (
+ reloadWidget(i, data)}
+ isChild={areChildren}
+ labelAdditionalComponentsRight={labelAdditionalComponentsRight}
+ labelAdditionalComponentsLeft={labelAdditionalComponentsLeft}
+ >
+
+
+ )
+ }
+ {
+ widgetMetaData.type === "block" && (
+ reloadWidget(i, data)}
+ isChild={areChildren}
+ labelAdditionalComponentsRight={labelAdditionalComponentsRight}
+ labelAdditionalComponentsLeft={labelAdditionalComponentsLeft}
+ >
+
+
+ )
+ }
{
widgetMetaData.type === "dataBagViewer" && (
widgetData && widgetData[i] && widgetData[i].queryParams &&
@@ -523,7 +576,6 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
renderedWidget = (
{renderedWidget}
diff --git a/src/qqq/components/widgets/ParentWidget.tsx b/src/qqq/components/widgets/ParentWidget.tsx
index dd0ac06..bc4a5b2 100644
--- a/src/qqq/components/widgets/ParentWidget.tsx
+++ b/src/qqq/components/widgets/ParentWidget.tsx
@@ -33,6 +33,7 @@ import Client from "qqq/utils/qqq/Client";
//////////////////////////////////////////////
export interface ParentWidgetData
{
+ label?: string;
dropdownLabelList: string[];
dropdownNameList: string[];
dropdownDataList: {
@@ -42,6 +43,7 @@ export interface ParentWidgetData
childWidgetNameList: string[];
dropdownNeedsSelectedText?: string;
storeDropdownSelections?: boolean;
+ csvData?: any[][];
icon?: string;
layoutType: string;
}
@@ -64,7 +66,8 @@ interface Props
const qController = Client.getInstance();
-function ParentWidget({urlParams, widgetMetaData, widgetIndex, data, reloadWidgetCallback, entityPrimaryKey, tableName, storeDropdownSelections}: Props, ): JSX.Element
+
+function ParentWidget({urlParams, widgetMetaData, widgetIndex, data, reloadWidgetCallback, entityPrimaryKey, tableName, storeDropdownSelections}: Props,): JSX.Element
{
const [childUrlParams, setChildUrlParams] = useState((urlParams) ? urlParams : "");
const [qInstance, setQInstance] = useState(null as QInstance);
@@ -81,27 +84,27 @@ function ParentWidget({urlParams, widgetMetaData, widgetIndex, data, reloadWidge
useEffect(() =>
{
- if(qInstance && data && data.childWidgetNameList)
+ if (qInstance && data && data.childWidgetNameList)
{
let widgetMetaDataList = [] as QWidgetMetaData[];
data?.childWidgetNameList.forEach((widgetName: string) =>
{
widgetMetaDataList.push(qInstance.widgets.get(widgetName));
- })
+ });
setWidgets(widgetMetaDataList);
}
}, [qInstance, data, childUrlParams]);
useEffect(() =>
{
- setChildUrlParams(urlParams)
+ setChildUrlParams(urlParams);
}, [urlParams]);
const parentReloadWidgetCallback = (data: string) =>
{
setChildUrlParams(data);
reloadWidgetCallback(data);
- }
+ };
///////////////////////////////////////////////////////////////////////////////////////////
// if this parent widget is in card form, and its children are too, then we need some px //
@@ -125,7 +128,7 @@ function ParentWidget({urlParams, widgetMetaData, widgetIndex, data, reloadWidge
omitPadding={omitPadding}
>
-
+
) : null
diff --git a/src/qqq/components/widgets/Widget.tsx b/src/qqq/components/widgets/Widget.tsx
index c2f3fcb..bc4838d 100644
--- a/src/qqq/components/widgets/Widget.tsx
+++ b/src/qqq/components/widgets/Widget.tsx
@@ -32,6 +32,8 @@ import React, {useEffect, useState} from "react";
import {NavigateFunction, useNavigate} from "react-router-dom";
import colors from "qqq/assets/theme/base/colors";
import WidgetDropdownMenu, {DropdownOption} from "qqq/components/widgets/components/WidgetDropdownMenu";
+import {WidgetUtils} from "qqq/components/widgets/WidgetUtils";
+import HtmlUtils from "qqq/utils/HtmlUtils";
export interface WidgetData
{
@@ -109,16 +111,18 @@ export class HeaderIcon extends LabelComponent
iconPath: string;
color: string;
coloredBG: boolean;
+ role: string;
iconColor: string;
bgColor: string;
- constructor(iconName: string, iconPath: string, color: string, coloredBG: boolean = true)
+ constructor(iconName: string, iconPath: string, color: string, role?: string, coloredBG: boolean = true)
{
super();
this.iconName = iconName;
this.iconPath = iconPath;
this.color = color;
+ this.role = role;
this.coloredBG = coloredBG;
this.iconColor = this.coloredBG ? "#FFFFFF" : this.color;
@@ -128,7 +132,7 @@ export class HeaderIcon extends LabelComponent
render = (args: LabelComponentRenderArgs): JSX.Element =>
{
- const styles = {
+ const styles: any = {
width: "1.75rem",
height: "1.75rem",
color: this.iconColor,
@@ -136,6 +140,12 @@ export class HeaderIcon extends LabelComponent
borderRadius: "0.25rem"
};
+ if(this.role == "topLeftInsideCard")
+ {
+ styles["order"] = -1;
+ styles["marginRight"] = "0.5rem";
+ }
+
if (this.iconPath)
{
return ();
@@ -317,11 +327,13 @@ export class ReloadControl extends LabelComponent
render = (args: LabelComponentRenderArgs): JSX.Element =>
{
- return (
-
-
-
- );
+ return (
+
+
+
+ );
};
}
@@ -336,15 +348,29 @@ function Widget(props: React.PropsWithChildren): JSX.Element
{
const navigate = useNavigate();
const [dropdownData, setDropdownData] = useState([]);
- const [fullScreenWidgetClassName, setFullScreenWidgetClassName] = useState("");
const [reloading, setReloading] = useState(false);
const [dropdownDataJSON, setDropdownDataJSON] = useState("");
const [labelComponentsLeft, setLabelComponentsLeft] = useState([] as LabelComponent[]);
const [labelComponentsRight, setLabelComponentsRight] = useState([] as LabelComponent[]);
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // support for using widget (data) label as page header, w/o it disappearing if dropdowns are changed //
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////
+ const [lastSeenLabel, setLastSeenLabel] = useState("");
+ const [usingLabelAsTitle, setUsingLabelAsTitle] = useState(false);
+
function renderComponent(component: LabelComponent, componentIndex: number)
{
- return component.render({navigate: navigate, widgetProps: props, dropdownData: dropdownData, componentIndex: componentIndex, reloadFunction: doReload});
+ if(component && component.render)
+ {
+ return component.render({navigate: navigate, widgetProps: props, dropdownData: dropdownData, componentIndex: componentIndex, reloadFunction: doReload});
+ }
+ else
+ {
+ console.log("Request to render a null component or component without a render function...");
+ console.log(JSON.stringify(component));
+ return (<>>);
+ }
}
useEffect(() =>
@@ -409,7 +435,10 @@ function Widget(props: React.PropsWithChildren): JSX.Element
{
defaultValue = props.widgetData.dropdownDefaultValueList[index];
}
- updatedStateLabelComponentsRight.push(new Dropdown(props.widgetData.dropdownLabelList[index], props.widgetMetaData.dropdowns[index], dropdownData, defaultValue, props.widgetData.dropdownNameList[index], handleDataChange));
+ if(props.widgetData?.dropdownLabelList && props.widgetData?.dropdownLabelList[index] && props.widgetMetaData?.dropdowns && props.widgetMetaData?.dropdowns[index] && props.widgetData?.dropdownNameList && props.widgetData?.dropdownNameList[index])
+ {
+ updatedStateLabelComponentsRight.push(new Dropdown(props.widgetData.dropdownLabelList[index], props.widgetMetaData.dropdowns[index], dropdownData, defaultValue, props.widgetData.dropdownNameList[index], handleDataChange));
+ }
});
setLabelComponentsRight(updatedStateLabelComponentsRight);
}
@@ -500,18 +529,35 @@ function Widget(props: React.PropsWithChildren): JSX.Element
}
};
- const toggleFullScreenWidget = () =>
+ const onExportClick = () =>
{
- if (fullScreenWidgetClassName)
+ if (props.widgetData?.csvData)
{
- setFullScreenWidgetClassName("");
+ const csv = WidgetUtils.widgetCsvDataToString(props.widgetData);
+ const fileName = WidgetUtils.makeExportFileName(props.widgetData, props.widgetMetaData);
+ HtmlUtils.download(fileName, csv);
}
else
{
- setFullScreenWidgetClassName("fullScreenWidget");
+ alert("There is no data available to export.");
}
};
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////
+ // add the export button to the label's left elements, if the meta-data says to show it //
+ // don't do this for 2 types which themselves add the button (and have custom code to do the export) //
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////
+ let localLabelAdditionalElementsLeft = [...props.labelAdditionalElementsLeft];
+ if (props.widgetMetaData?.showExportButton && props.widgetMetaData?.type !== "table" && props.widgetMetaData?.type !== "childRecordList")
+ {
+ if(!localLabelAdditionalElementsLeft)
+ {
+ localLabelAdditionalElementsLeft = [];
+ }
+
+ localLabelAdditionalElementsLeft.push(WidgetUtils.generateExportButton(onExportClick));
+ }
+
const hasPermission = props.widgetData?.hasPermission === undefined || props.widgetData?.hasPermission === true;
const isSet = (v: any): boolean =>
@@ -526,36 +572,56 @@ function Widget(props: React.PropsWithChildren): JSX.Element
if (hasPermission)
{
needLabelBox ||= (labelComponentsLeft && labelComponentsLeft.length > 0);
- needLabelBox ||= (props.labelAdditionalElementsLeft && props.labelAdditionalElementsLeft.length > 0);
+ needLabelBox ||= (localLabelAdditionalElementsLeft && localLabelAdditionalElementsLeft.length > 0);
needLabelBox ||= (labelComponentsRight && labelComponentsRight.length > 0);
- needLabelBox ||= isSet(props.widgetMetaData?.icon);
+ needLabelBox ||= isSet(props.widgetData?.icon);
needLabelBox ||= isSet(props.widgetData?.label);
needLabelBox ||= isSet(props.widgetMetaData?.label);
}
//////////////////////////////////////////////////////////////////////////////////////////
// first look for a label in the widget data, which would override that in the metadata //
- // note - previously this had a ?: and one was pl={2}, the other was pl={3}... //
//////////////////////////////////////////////////////////////////////////////////////////
- const labelToUse = props.widgetData?.label ?? props.widgetMetaData?.label;
+ const isParentWidget = props.widgetMetaData.type == "parentWidget"; // todo - do we need to know top-level parent, vs. a nested parent?
+ let labelToUse = props.widgetData?.label ?? props.widgetMetaData?.label;
+
+ if(!labelToUse)
+ {
+ /////////////////////////////////////////////////////////////////////////////////////////////
+ // prevent the label from disappearing, especially when it's being used as the page header //
+ /////////////////////////////////////////////////////////////////////////////////////////////
+ if(lastSeenLabel && isParentWidget && usingLabelAsTitle)
+ {
+ labelToUse = lastSeenLabel;
+ }
+ }
+
let labelElement = (
-
+
{labelToUse}
);
+ if(labelToUse && labelToUse != lastSeenLabel)
+ {
+ setLastSeenLabel(labelToUse)
+ setUsingLabelAsTitle(props.widgetData.isLabelPageTitle);
+ }
+
if (props.widgetMetaData.tooltip)
{
labelElement = {labelElement};
}
+ const isTable = props.widgetMetaData.type == "table";
+
const errorLoading = props.widgetData?.errorLoading !== undefined && props.widgetData?.errorLoading === true;
const widgetContent =
{
needLabelBox &&
-
+
{
hasPermission ?
props.widgetMetaData?.icon && (
@@ -600,11 +666,11 @@ function Widget(props: React.PropsWithChildren): JSX.Element
hasPermission && (
labelComponentsLeft.map((component, i) =>
{
- return ({renderComponent(component, i)});
+ return ({renderComponent(component, i)});
})
)
}
- {props.labelAdditionalElementsLeft}
+ {localLabelAdditionalElementsLeft}
{
@@ -650,17 +716,27 @@ function Widget(props: React.PropsWithChildren): JSX.Element
}
{
!errorLoading && props?.footerHTML && (
- {parse(props.footerHTML)}
+ {parse(props.footerHTML)}
)
}
;
const padding = props.omitPadding ? "auto" : "24px 16px";
+
+ ///////////////////////////////////////////////////
+ // try to make tables fill their entire "parent" //
+ ///////////////////////////////////////////////////
+ let noCardMarginBottom = "unset";
+ if(isTable)
+ {
+ noCardMarginBottom = "-8px";
+ }
+
return props.widgetMetaData?.isCard
- ?
+ ?
{widgetContent}
- : {widgetContent};
+ : {widgetContent};
}
export default Widget;
diff --git a/src/qqq/components/widgets/WidgetBlock.tsx b/src/qqq/components/widgets/WidgetBlock.tsx
new file mode 100644
index 0000000..fab8f6d
--- /dev/null
+++ b/src/qqq/components/widgets/WidgetBlock.tsx
@@ -0,0 +1,90 @@
+/*
+ * 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 {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
+import {Alert, Skeleton} from "@mui/material";
+import React from "react";
+import BigNumberBlock from "qqq/components/widgets/blocks/BigNumberBlock";
+import {BlockData} from "qqq/components/widgets/blocks/BlockModels";
+import DividerBlock from "qqq/components/widgets/blocks/DividerBlock";
+import NumberIconBadgeBlock from "qqq/components/widgets/blocks/NumberIconBadgeBlock";
+import ProgressBarBlock from "qqq/components/widgets/blocks/ProgressBarBlock";
+import TableSubRowDetailRowBlock from "qqq/components/widgets/blocks/TableSubRowDetailRowBlock";
+import TextBlock from "qqq/components/widgets/blocks/TextBlock";
+import UpOrDownNumberBlock from "qqq/components/widgets/blocks/UpOrDownNumberBlock";
+import CompositeWidget from "qqq/components/widgets/CompositeWidget";
+
+
+interface WidgetBlockProps
+{
+ widgetMetaData: QWidgetMetaData;
+ block: BlockData;
+}
+
+
+/*******************************************************************************
+ ** Component to render a single Block in the widget framework!
+ *******************************************************************************/
+export default function WidgetBlock({widgetMetaData, block}: WidgetBlockProps): JSX.Element
+{
+ if(!block)
+ {
+ return ();
+ }
+
+ if(!block.values)
+ {
+ block.values = {};
+ }
+
+ if(!block.styles)
+ {
+ block.styles = {};
+ }
+
+ if(block.blockTypeName == "COMPOSITE")
+ {
+ // @ts-ignore - special case for composite type block...
+ return ();
+ }
+
+ switch(block.blockTypeName)
+ {
+ case "TEXT":
+ return ();
+ case "NUMBER_ICON_BADGE":
+ return ();
+ case "UP_OR_DOWN_NUMBER":
+ return ();
+ case "TABLE_SUB_ROW_DETAIL_ROW":
+ return ();
+ case "PROGRESS_BAR":
+ return ();
+ case "DIVIDER":
+ return ();
+ case "BIG_NUMBER":
+ return ();
+ default:
+ return (Unsupported block type: {block.blockTypeName})
+ }
+
+}
diff --git a/src/qqq/components/widgets/WidgetUtils.tsx b/src/qqq/components/widgets/WidgetUtils.tsx
new file mode 100644
index 0000000..3e81b54
--- /dev/null
+++ b/src/qqq/components/widgets/WidgetUtils.tsx
@@ -0,0 +1,100 @@
+/*
+ * 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 {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
+import Button from "@mui/material/Button";
+import Icon from "@mui/material/Icon";
+import Tooltip from "@mui/material/Tooltip/Tooltip";
+import Typography from "@mui/material/Typography";
+import React from "react";
+import colors from "qqq/assets/theme/base/colors";
+import {WidgetData} from "qqq/components/widgets/Widget";
+import ValueUtils from "qqq/utils/qqq/ValueUtils";
+
+/*******************************************************************************
+ ** Utility class used by Widgets
+ **
+ *******************************************************************************/
+export class WidgetUtils
+{
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static generateExportButton = (onExportClick: () => void): JSX.Element =>
+ {
+ return (
+
+
+
+ );
+ };
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static widgetCsvDataToString = (data: WidgetData): string =>
+ {
+ function isNumeric(x: any)
+ {
+ return !isNaN(Number(x));
+ }
+
+ let csv = "";
+ for (let i = 0; i < data.csvData.length; i++)
+ {
+ for (let j = 0; j < data.csvData[i].length; j++)
+ {
+ if (j > 0)
+ {
+ csv += ",";
+ }
+
+ let cell = data.csvData[i][j];
+ if (cell && isNumeric(String(cell)))
+ {
+ csv += cell;
+ }
+ else
+ {
+ csv += `"${ValueUtils.cleanForCsv(cell)}"`;
+ }
+ }
+ csv += "\n";
+ }
+
+ return (csv);
+ };
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static makeExportFileName = (data: WidgetData, widgetMetaData: QWidgetMetaData): string =>
+ {
+ const fileName = (data?.label ?? widgetMetaData.label) + " " + ValueUtils.formatDateTimeForFileName(new Date()) + ".csv";
+ return (fileName);
+ };
+
+}
\ No newline at end of file
diff --git a/src/qqq/components/widgets/blocks/BigNumberBlock.tsx b/src/qqq/components/widgets/blocks/BigNumberBlock.tsx
new file mode 100644
index 0000000..c541565
--- /dev/null
+++ b/src/qqq/components/widgets/blocks/BigNumberBlock.tsx
@@ -0,0 +1,69 @@
+/*
+ * 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 BlockElementWrapper from "qqq/components/widgets/blocks/BlockElementWrapper";
+import {StandardBlockComponentProps} from "qqq/components/widgets/blocks/BlockModels";
+import UpOrDownNumberBlock from "qqq/components/widgets/blocks/UpOrDownNumberBlock";
+
+
+
+/*******************************************************************************
+ ** Block that renders ... a big number, optionally with some other stuff.
+ **
+ ** ${heading}
+ ** ${number} ${context}
+ *******************************************************************************/
+export default function BigNumberBlock({widgetMetaData, data}: StandardBlockComponentProps): JSX.Element
+{
+ let flexJustifyContent = "normal";
+ let flexAlignItems = "baseline";
+
+ return (
+
+
+
+
+ {data.values.heading}
+
+
+
+
+
+
+
+
+ {data.values.number}
+
+
+ {
+ data.values.context &&
+
+
+ {data.values.context}
+
+
+ }
+
+
+
+
+ );
+}
diff --git a/src/qqq/components/widgets/blocks/BlockElementWrapper.tsx b/src/qqq/components/widgets/blocks/BlockElementWrapper.tsx
new file mode 100644
index 0000000..5e0352a
--- /dev/null
+++ b/src/qqq/components/widgets/blocks/BlockElementWrapper.tsx
@@ -0,0 +1,80 @@
+/*
+ * 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 {Tooltip} from "@mui/material";
+import React, {ReactElement} from "react";
+import {Link} from "react-router-dom";
+import {BlockData, BlockLink, BlockTooltip} from "qqq/components/widgets/blocks/BlockModels";
+
+interface BlockElementWrapperProps
+{
+ data: BlockData;
+ slot: string
+ linkProps?: any;
+ children: ReactElement;
+}
+
+/*******************************************************************************
+ ** For Blocks - wrap their "slot" elements with an optional tooltip and/or link
+ *******************************************************************************/
+export default function BlockElementWrapper({data, slot, linkProps, children}: BlockElementWrapperProps): JSX.Element
+{
+ let link: BlockLink;
+ let tooltip: BlockTooltip;
+
+ if(slot)
+ {
+ link = data.linkMap && data.linkMap[slot.toUpperCase()];
+ if(!link)
+ {
+ link = data.link;
+ }
+
+ tooltip = data.tooltipMap && data.tooltipMap[slot.toUpperCase()];
+ if(!tooltip)
+ {
+ tooltip = data.tooltip;
+ }
+ }
+ else
+ {
+ link = data.link;
+ tooltip = data.tooltip;
+ }
+
+ let rs = children;
+
+ if(link)
+ {
+ rs = {rs}
+ }
+
+ if(tooltip)
+ {
+ let placement = tooltip.placement ? tooltip.placement.toLowerCase() : "bottom"
+
+ // @ts-ignore - placement possible values
+ rs = {rs}
+ }
+
+ return (rs);
+}
diff --git a/src/qqq/components/widgets/blocks/BlockModels.ts b/src/qqq/components/widgets/blocks/BlockModels.ts
new file mode 100644
index 0000000..ed6078a
--- /dev/null
+++ b/src/qqq/components/widgets/blocks/BlockModels.ts
@@ -0,0 +1,58 @@
+/*
+ * 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 {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
+
+
+export interface BlockData
+{
+ blockTypeName: string;
+
+ tooltip?: BlockTooltip;
+ link?: BlockLink;
+ tooltipMap?: {[slot: string]: BlockTooltip};
+ linkMap?: {[slot: string]: BlockLink};
+
+ values: any;
+ styles?: any;
+}
+
+
+export interface BlockTooltip
+{
+ title: string;
+ placement: string;
+}
+
+
+export interface BlockLink
+{
+ href: string;
+ target: string;
+}
+
+
+export interface StandardBlockComponentProps
+{
+ widgetMetaData: QWidgetMetaData;
+ data: BlockData;
+}
+
diff --git a/src/qqq/components/widgets/blocks/DividerBlock.tsx b/src/qqq/components/widgets/blocks/DividerBlock.tsx
new file mode 100644
index 0000000..3897f2f
--- /dev/null
+++ b/src/qqq/components/widgets/blocks/DividerBlock.tsx
@@ -0,0 +1,33 @@
+/*
+ * 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 {StandardBlockComponentProps} from "qqq/components/widgets/blocks/BlockModels";
+
+
+/*******************************************************************************
+ ** Block that renders a simple dividing line
+ ** margins & width are set such that it covers the padding of a card.
+ ** if we need to use it differently, a style attribute should be added to its backend data.
+ *******************************************************************************/
+export default function DividerBlock({}: StandardBlockComponentProps): JSX.Element
+{
+ return ();
+}
diff --git a/src/qqq/components/widgets/blocks/NumberIconBadgeBlock.tsx b/src/qqq/components/widgets/blocks/NumberIconBadgeBlock.tsx
new file mode 100644
index 0000000..f6cc116
--- /dev/null
+++ b/src/qqq/components/widgets/blocks/NumberIconBadgeBlock.tsx
@@ -0,0 +1,48 @@
+/*
+ * 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 Icon from "@mui/material/Icon";
+import BlockElementWrapper from "qqq/components/widgets/blocks/BlockElementWrapper";
+import {StandardBlockComponentProps} from "qqq/components/widgets/blocks/BlockModels";
+
+/*******************************************************************************
+ ** Block that renders ... a number, and an icon, like a badge.
+ **
+ ** ${number} ${icon}
+ *******************************************************************************/
+export default function NumberIconBadgeBlock({data}: StandardBlockComponentProps): JSX.Element
+{
+ return (
+
);
+}
diff --git a/src/qqq/components/widgets/blocks/ProgressBarBlock.tsx b/src/qqq/components/widgets/blocks/ProgressBarBlock.tsx
new file mode 100644
index 0000000..fbf1afc
--- /dev/null
+++ b/src/qqq/components/widgets/blocks/ProgressBarBlock.tsx
@@ -0,0 +1,70 @@
+/*
+ * 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 Typography from "@mui/material/Typography";
+import BlockElementWrapper from "qqq/components/widgets/blocks/BlockElementWrapper";
+import {StandardBlockComponentProps} from "qqq/components/widgets/blocks/BlockModels";
+
+
+/*******************************************************************************
+ ** Block that renders a progress bar!
+ **
+ ** Values:
+ ** ${heading}
+ ** [${percent}===___] ${value ?? percent}
+ **
+ ** Slots:
+ ** ${heading}
+ ** ${bar} ${value}
+ *******************************************************************************/
+export default function ProgressBarBlock({data}: StandardBlockComponentProps): JSX.Element
+{
+ return (
+
+ {
+ data.values.heading &&
+
+
+ {data.values.heading}
+
+
+ }
+
+
+
+
+
+ {
+ data.values.percent > 0 ? : <>>
+ }
+
+
+
+
+
+ {data.values.value ?? `${(data.values.percent as number).toFixed(1)}%`}
+
+
+
+
+ );
+
+}
diff --git a/src/qqq/components/widgets/blocks/TableSubRowDetailRowBlock.tsx b/src/qqq/components/widgets/blocks/TableSubRowDetailRowBlock.tsx
new file mode 100644
index 0000000..4f52b65
--- /dev/null
+++ b/src/qqq/components/widgets/blocks/TableSubRowDetailRowBlock.tsx
@@ -0,0 +1,54 @@
+/*
+ * 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 BlockElementWrapper from "qqq/components/widgets/blocks/BlockElementWrapper";
+import {StandardBlockComponentProps} from "qqq/components/widgets/blocks/BlockModels";
+
+
+/*******************************************************************************
+ ** Block that renders a label & value, meant to be used as a detail-row in a
+ ** sub-row within a table widget
+ **
+ ** ${label} ${value}
+ *******************************************************************************/
+export default function TableSubRowDetailRowBlock({data}: StandardBlockComponentProps): JSX.Element
+{
+ return (
+
+ );
+}
diff --git a/src/qqq/components/widgets/blocks/TextBlock.tsx b/src/qqq/components/widgets/blocks/TextBlock.tsx
new file mode 100644
index 0000000..a10f3c1
--- /dev/null
+++ b/src/qqq/components/widgets/blocks/TextBlock.tsx
@@ -0,0 +1,37 @@
+/*
+ * 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 BlockElementWrapper from "qqq/components/widgets/blocks/BlockElementWrapper";
+import {StandardBlockComponentProps} from "qqq/components/widgets/blocks/BlockModels";
+
+/*******************************************************************************
+ ** Block that renders ... just some text.
+ **
+ ** ${text}
+ *******************************************************************************/
+export default function TextBlock({data}: StandardBlockComponentProps): JSX.Element
+{
+ return (
+
+ {data.values.text}
+
+ );
+}
diff --git a/src/qqq/components/widgets/blocks/UpOrDownNumberBlock.tsx b/src/qqq/components/widgets/blocks/UpOrDownNumberBlock.tsx
new file mode 100644
index 0000000..b5aafdd
--- /dev/null
+++ b/src/qqq/components/widgets/blocks/UpOrDownNumberBlock.tsx
@@ -0,0 +1,81 @@
+/*
+ * 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 Icon from "@mui/material/Icon";
+import React from "react";
+import BlockElementWrapper from "qqq/components/widgets/blocks/BlockElementWrapper";
+import {StandardBlockComponentProps} from "qqq/components/widgets/blocks/BlockModels";
+
+
+/*******************************************************************************
+ ** Block that renders an up/down icon, a number, and some context
+ **
+ ** ${icon} ${number} ${context}
+ *
+ ** or, if style.isStacked:
+ *
+ ** ${icon} ${number}
+ ** ${context}
+ *******************************************************************************/
+export default function UpOrDownNumberBlock({data}: StandardBlockComponentProps): JSX.Element
+{
+ if (!data.styles)
+ {
+ data.styles = {};
+ }
+
+ if (!data.values)
+ {
+ data.values = {};
+ }
+
+ const UP_ICON = "arrow_drop_up";
+ const DOWN_ICON = "arrow_drop_down";
+
+ const defaultGreenColor = "#2BA83F";
+ const defaultRedColor = "#FB4141";
+
+ const goodOrBadColor = data.styles.colorOverride ?? (data.values.isGood ? defaultGreenColor : defaultRedColor);
+ const iconName = data.values.isUp ? UP_ICON : DOWN_ICON;
+
+ return (
+ <>
+