diff --git a/package.json b/package.json index 38b4755..c3907f4 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "@auth0/auth0-react": "1.10.2", "@emotion/react": "11.7.1", "@emotion/styled": "11.6.0", - "@kingsrook/qqq-frontend-core": "1.0.85", + "@kingsrook/qqq-frontend-core": "1.0.86", "@mui/icons-material": "5.4.1", "@mui/material": "5.11.1", "@mui/styles": "5.11.1", diff --git a/pom.xml b/pom.xml index 3b1d74f..e8fab8e 100644 --- a/pom.xml +++ b/pom.xml @@ -66,7 +66,7 @@ com.kingsrook.qqq qqq-backend-core - feature-CE-798-quick-filters-20240123.205854-1 + 0.20.0-SNAPSHOT org.slf4j diff --git a/src/main/java/com/kingsrook/qqq/frontend/materialdashboard/model/metadata/MaterialDashboardAppMetaData.java b/src/main/java/com/kingsrook/qqq/frontend/materialdashboard/model/metadata/MaterialDashboardAppMetaData.java new file mode 100644 index 0000000..2aee1fd --- /dev/null +++ b/src/main/java/com/kingsrook/qqq/frontend/materialdashboard/model/metadata/MaterialDashboardAppMetaData.java @@ -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 . + */ + +package com.kingsrook.qqq.frontend.materialdashboard.model.metadata; + + +import com.kingsrook.qqq.backend.core.model.metadata.layout.QSupplementalAppMetaData; + + +/******************************************************************************* + ** app-level meta-data for this module (handled as QSupplementalTableMetaData) + *******************************************************************************/ +public class MaterialDashboardAppMetaData extends QSupplementalAppMetaData +{ + private Boolean showAppLabelOnHomeScreen = true; + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public boolean includeInFullFrontendMetaData() + { + return (true); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public String getType() + { + return ("materialDashboard"); + } + + + + /******************************************************************************* + ** Getter for showAppLabelOnHomeScreen + *******************************************************************************/ + public Boolean getShowAppLabelOnHomeScreen() + { + return (this.showAppLabelOnHomeScreen); + } + + + + /******************************************************************************* + ** Setter for showAppLabelOnHomeScreen + *******************************************************************************/ + public void setShowAppLabelOnHomeScreen(Boolean showAppLabelOnHomeScreen) + { + this.showAppLabelOnHomeScreen = showAppLabelOnHomeScreen; + } + + + + /******************************************************************************* + ** Fluent setter for showAppLabelOnHomeScreen + *******************************************************************************/ + public MaterialDashboardAppMetaData withShowAppLabelOnHomeScreen(Boolean showAppLabelOnHomeScreen) + { + this.showAppLabelOnHomeScreen = showAppLabelOnHomeScreen; + return (this); + } + +} diff --git a/src/main/java/com/kingsrook/qqq/frontend/materialdashboard/model/metadata/MaterialDashboardIconRoleNames.java b/src/main/java/com/kingsrook/qqq/frontend/materialdashboard/model/metadata/MaterialDashboardIconRoleNames.java index 6d1377d..9192089 100644 --- a/src/main/java/com/kingsrook/qqq/frontend/materialdashboard/model/metadata/MaterialDashboardIconRoleNames.java +++ b/src/main/java/com/kingsrook/qqq/frontend/materialdashboard/model/metadata/MaterialDashboardIconRoleNames.java @@ -28,4 +28,5 @@ package com.kingsrook.qqq.frontend.materialdashboard.model.metadata; public interface MaterialDashboardIconRoleNames { String TOP_RIGHT_INSIDE_CARD = "topRightInsideCard"; + String TOP_LEFT_INSIDE_CARD = "topLeftInsideCard"; } diff --git a/src/qqq/components/widgets/CompositeWidget.tsx b/src/qqq/components/widgets/CompositeWidget.tsx new file mode 100644 index 0000000..d118529 --- /dev/null +++ b/src/qqq/components/widgets/CompositeWidget.tsx @@ -0,0 +1,109 @@ +/* + * 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 {Box, Skeleton} from "@mui/material"; +import React from "react"; +import {BlockData} from "qqq/components/widgets/blocks/BlockModels"; +import WidgetBlock from "qqq/components/widgets/WidgetBlock"; + + +interface CompositeData +{ + blocks: BlockData[]; + styleOverrides?: any; + layout?: string +} + + +interface CompositeWidgetProps +{ + widgetMetaData: QWidgetMetaData; + data: CompositeData; +} + + +/******************************************************************************* + ** Widget which is a list of Blocks. + *******************************************************************************/ +export default function CompositeWidget({widgetMetaData, data}: CompositeWidgetProps): JSX.Element +{ + if (!data || !data.blocks) + { + return (); + } + + //////////////////////////////////////////////////////////////////////////////////// + // note - these layouts are defined in qqq in the CompositeWidgetData.Layout enum // + //////////////////////////////////////////////////////////////////////////////////// + let layout = data?.layout ?? widgetMetaData?.layout + let boxStyle: any = {}; + if (layout == "FLEX_ROW_WRAPPED") + { + boxStyle.display = "flex"; + boxStyle.flexDirection = "row"; + boxStyle.flexWrap = "wrap"; + boxStyle.gap = "0.5rem"; + } + else if (layout == "FLEX_ROW_SPACE_BETWEEN") + { + boxStyle.display = "flex"; + boxStyle.flexDirection = "row"; + boxStyle.justifyContent = "space-between" + boxStyle.gap = "0.25rem"; + } + else if (layout == "TABLE_SUB_ROW_DETAILS") + { + boxStyle.display = "flex"; + boxStyle.flexDirection = "column"; + boxStyle.fontSize = "0.875rem"; + boxStyle.fontWeight = 400; + boxStyle.borderRight = "1px solid #D0D0D0"; + } + else if (layout == "BADGES_WRAPPER") + { + boxStyle.display = "flex"; + boxStyle.gap = "0.25rem"; + boxStyle.padding = "0 0.25rem"; + boxStyle.fontSize = "0.875rem"; + boxStyle.fontWeight = 400; + boxStyle.border = "1px solid gray"; + boxStyle.borderRadius = "0.5rem"; + boxStyle.background = "#FFFFFF"; + } + + if (data?.styleOverrides) + { + boxStyle = {...boxStyle, ...data.styleOverrides}; + } + + return ( + { + data.blocks.map((block: BlockData, index) => ( + + + + )) + } + ); + +} diff --git a/src/qqq/components/widgets/DashboardWidgets.tsx b/src/qqq/components/widgets/DashboardWidgets.tsx index 28b3858..6c5ab04 100644 --- a/src/qqq/components/widgets/DashboardWidgets.tsx +++ b/src/qqq/components/widgets/DashboardWidgets.tsx @@ -35,6 +35,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 CompositeWidget from "qqq/components/widgets/CompositeWidget"; import DataBagViewer from "qqq/components/widgets/misc/DataBagViewer"; import DividerWidget from "qqq/components/widgets/misc/Divider"; import FieldValueListWidget from "qqq/components/widgets/misc/FieldValueListWidget"; @@ -47,6 +48,7 @@ import ParentWidget from "qqq/components/widgets/ParentWidget"; import MultiStatisticsCard from "qqq/components/widgets/statistics/MultiStatisticsCard"; import StatisticsCard from "qqq/components/widgets/statistics/StatisticsCard"; import Widget, {HeaderIcon, WIDGET_DROPDOWN_SELECTION_LOCAL_STORAGE_KEY_ROOT, LabelComponent} from "qqq/components/widgets/Widget"; +import WidgetBlock from "qqq/components/widgets/WidgetBlock"; import ProcessRun from "qqq/pages/processes/ProcessRun"; import Client from "qqq/utils/qqq/Client"; import TableWidget from "./tables/TableWidget"; @@ -254,7 +256,17 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit const topRightInsideCardIcon = widgetMetaData.icons.get("topRightInsideCard"); if (topRightInsideCardIcon) { - labelAdditionalComponentsRight.push(new HeaderIcon(topRightInsideCardIcon.name, topRightInsideCardIcon.path, topRightInsideCardIcon.color)); + labelAdditionalComponentsRight.push(new HeaderIcon(topRightInsideCardIcon.name, topRightInsideCardIcon.path, topRightInsideCardIcon.color, "topRightInsideCard")); + } + } + + const labelAdditionalComponentsLeft: LabelComponent[] = []; + if (widgetMetaData && widgetMetaData.icons) + { + const topLeftInsideCardIcon = widgetMetaData.icons.get("topLeftInsideCard"); + if (topLeftInsideCardIcon) + { + labelAdditionalComponentsLeft.push(new HeaderIcon(topLeftInsideCardIcon.name, topLeftInsideCardIcon.path, topLeftInsideCardIcon.color, "topLeftInsideCard")); } } @@ -302,6 +314,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit reloadWidgetCallback={(data) => reloadWidget(i, data)} isChild={areChildren} labelAdditionalComponentsRight={labelAdditionalComponentsRight} + labelAdditionalComponentsLeft={labelAdditionalComponentsLeft} > @@ -314,6 +327,8 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit widgetData={widgetData[i]} reloadWidgetCallback={(data) => reloadWidget(i, data)} showReloadControl={false} + labelAdditionalComponentsRight={labelAdditionalComponentsRight} + labelAdditionalComponentsLeft={labelAdditionalComponentsLeft} >
@@ -327,6 +342,8 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit widgetMetaData={widgetMetaData} widgetData={widgetData[i]} reloadWidgetCallback={(data) => reloadWidget(i, data)} + labelAdditionalComponentsRight={labelAdditionalComponentsRight} + labelAdditionalComponentsLeft={labelAdditionalComponentsLeft} > @@ -342,6 +359,8 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit widgetMetaData={widgetMetaData} reloadWidgetCallback={(data) => reloadWidget(i, data)} widgetData={widgetData[i]} + labelAdditionalComponentsRight={labelAdditionalComponentsRight} + labelAdditionalComponentsLeft={labelAdditionalComponentsLeft} > @@ -373,8 +392,11 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit widgetData={widgetData[i]} isChild={areChildren} reloadWidgetCallback={(data) => reloadWidget(i, data)} + labelAdditionalComponentsRight={labelAdditionalComponentsRight} + labelAdditionalComponentsLeft={labelAdditionalComponentsLeft} > @@ -414,6 +436,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit reloadWidgetCallback={(data) => reloadWidget(i, data)} isChild={areChildren} labelAdditionalComponentsRight={labelAdditionalComponentsRight} + labelAdditionalComponentsLeft={labelAdditionalComponentsLeft} >
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 ( +
+ { + data.values.number && + + {data.values.number} + + } + { + data.values.iconName && + + {data.values.iconName} + + } +
); +} 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 ( +
+ + { + data.values.label && +
+ + {data.values.label} + +
+ } + + { + data.values.value && + + {data.values.value} + + } +
+ ); +} 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 ( + <> +
+ +
+ + <> + {iconName} + {data.values.number} + + +
+ +
+ + {data.values.context} + +
+ +
+ + ); +} diff --git a/src/qqq/components/widgets/misc/RecordGridWidget.tsx b/src/qqq/components/widgets/misc/RecordGridWidget.tsx index 7c3239c..602a26d 100644 --- a/src/qqq/components/widgets/misc/RecordGridWidget.tsx +++ b/src/qqq/components/widgets/misc/RecordGridWidget.tsx @@ -178,7 +178,7 @@ function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element if(widgetMetaData?.showExportButton) { labelAdditionalElementsLeft.push( - + ); diff --git a/src/qqq/components/widgets/tables/DataTable.tsx b/src/qqq/components/widgets/tables/DataTable.tsx index f697917..023b4a3 100644 --- a/src/qqq/components/widgets/tables/DataTable.tsx +++ b/src/qqq/components/widgets/tables/DataTable.tsx @@ -18,6 +18,7 @@ * along with this program. If not, see . */ +import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData"; import {tooltipClasses, TooltipProps} from "@mui/material"; import Autocomplete from "@mui/material/Autocomplete"; import Box from "@mui/material/Box"; @@ -35,11 +36,13 @@ import colors from "qqq/assets/theme/base/colors"; import MDInput from "qqq/components/legacy/MDInput"; import MDPagination from "qqq/components/legacy/MDPagination"; import MDTypography from "qqq/components/legacy/MDTypography"; +import CompositeWidget from "qqq/components/widgets/CompositeWidget"; import DataTableBodyCell from "qqq/components/widgets/tables/cells/DataTableBodyCell"; import DataTableHeadCell from "qqq/components/widgets/tables/cells/DataTableHeadCell"; import DefaultCell from "qqq/components/widgets/tables/cells/DefaultCell"; import ImageCell from "qqq/components/widgets/tables/cells/ImageCell"; import {TableDataInput} from "qqq/components/widgets/tables/TableCard"; +import WidgetBlock from "qqq/components/widgets/WidgetBlock"; interface Props { @@ -57,6 +60,7 @@ interface Props }; isSorted?: boolean; noEndBorder?: boolean; + widgetMetaData: QWidgetMetaData; } DataTable.defaultProps = { @@ -92,6 +96,7 @@ function DataTable({ pagination, isSorted, noEndBorder, + widgetMetaData }: Props): JSX.Element { let defaultValue: any; @@ -280,21 +285,36 @@ function DataTable({ entriesEnd = pageSize * (pageIndex + 1); } + let visibleFooterRows = 1; + if(expanded && expanded[`${table.rows.length-1}`]) + { + ////////////////////////////////////////////////// + // todo - should count how many are expanded... // + ////////////////////////////////////////////////// + visibleFooterRows = 2; + } + function getTable(includeHead: boolean, rows: any, isFooter: boolean) { let boxStyle = {}; if(fixedStickyLastRow) { boxStyle = isFooter - ? {borderTop: `0.0625rem solid ${colors.grayLines.main};`, overflow: "auto", scrollbarGutter: "stable"} - : {height: fixedHeight ? `${fixedHeight}px` : "360px", overflowY: "scroll", scrollbarGutter: "stable", marginBottom: "-1px"}; + ? {borderTop: `0.0625rem solid ${colors.grayLines.main};`, backgroundColor: "#EEEEEE"} + : {flexGrow: 1, overflowY: "scroll", scrollbarGutter: "stable", marginBottom: "-1px"}; } - return + let innerBoxStyle = {}; + if(fixedStickyLastRow && isFooter) + { + innerBoxStyle = {overflowY: "auto", scrollbarGutter: "stable"}; + } + + return { includeHead && ( - + {headerGroups.map((headerGroup: any, i: number) => ( {headerGroup.headers.map((column: any) => ( @@ -341,13 +361,23 @@ function DataTable({ overrideNoEndBorder = true; } + let background = "initial"; + if(isFooter) + { + background = "#EEEEEE"; + } + else if(row.depth > 0 || row.isExpanded) + { + background = "#FAFAFA"; + } + return ( - 0 ? "#FAFAFA" : "initial")}} key={key} {...row.getRowProps()}> + {row.cells.map((cell: any) => ( cell.column.type !== "hidden" && ( {parse(cell.value)} + {parse(cell.value ?? "")} + ) + } + { + cell.column.type === "composite" && ( + + + + ) + } + { + cell.column.type === "block" && ( + + + ) } { @@ -397,11 +441,11 @@ function DataTable({
-
+
} return ( - + {entriesPerPage && ((hidePaginationDropdown !== undefined && !hidePaginationDropdown) || canSearch) ? ( {entriesPerPage && (hidePaginationDropdown === undefined || !hidePaginationDropdown) && ( @@ -448,14 +492,16 @@ function DataTable({ ) : null} - { - fixedStickyLastRow ? ( - <> - {getTable(true, page.slice(0, page.length -1), false)} - {getTable(false, page.slice(page.length-1), true)} - - ) : getTable(true, page, false) - } + + { + fixedStickyLastRow ? ( + <> + {getTable(true, page.slice(0, page.length - visibleFooterRows), false)} + {getTable(false, page.slice(page.length - visibleFooterRows), true)} + + ) : getTable(true, page, false) + } + + { data && data.columns && !noRowsFoundHTML ? : noRowsFoundHTML ? - + { - if(csv) + if(props.widgetData?.csvData) + { + const csv = WidgetUtils.widgetCsvDataToString(props.widgetData); + const fileName = WidgetUtils.makeExportFileName(props.widgetData, props.widgetMetaData); + HtmlUtils.download(fileName, csv); + } + else if(csv) { HtmlUtils.download(fileName, csv); } @@ -128,11 +130,7 @@ function TableWidget(props: Props): JSX.Element const labelAdditionalElementsLeft: JSX.Element[] = []; if(props.widgetMetaData?.showExportButton) { - labelAdditionalElementsLeft.push( - - - - ); + labelAdditionalElementsLeft.push(WidgetUtils.generateExportButton(onExportClick)); } return ( @@ -151,6 +149,7 @@ function TableWidget(props: Props): JSX.Element fixedStickyLastRow={props.widgetData?.fixedStickyLastRow} fixedHeight={props.widgetData?.fixedHeight} data={{columns: props.widgetData?.columns, rows: props.widgetData?.rows}} + widgetMetaData={props.widgetMetaData} /> ); diff --git a/src/qqq/components/widgets/tables/cells/DataTableBodyCell.tsx b/src/qqq/components/widgets/tables/cells/DataTableBodyCell.tsx index 46c2ad1..22be101 100644 --- a/src/qqq/components/widgets/tables/cells/DataTableBodyCell.tsx +++ b/src/qqq/components/widgets/tables/cells/DataTableBodyCell.tsx @@ -39,7 +39,7 @@ function DataTableBodyCell({noBorder, align, children}: Props): JSX.Element component="td" textAlign={align} py={1.5} - px={3} + px={1.5} sx={({palette: {light}, typography: {size}, borders: {borderWidth}}: Theme) => ({ borderBottom: noBorder ? "none" : `${borderWidth[1]} solid ${colors.grayLines.main}`, fontSize: "0.875rem", diff --git a/src/qqq/components/widgets/tables/cells/DataTableHeadCell.tsx b/src/qqq/components/widgets/tables/cells/DataTableHeadCell.tsx index 20583f0..b9a9c0c 100644 --- a/src/qqq/components/widgets/tables/cells/DataTableHeadCell.tsx +++ b/src/qqq/components/widgets/tables/cells/DataTableHeadCell.tsx @@ -45,7 +45,7 @@ function DataTableHeadCell({width, children, sorted, align, ...rest}: Props): JS component="th" width={width} py={1.5} - px={3} + px={1.5} sx={({palette: {light}, borders: {borderWidth}}: Theme) => ({ borderBottom: `${borderWidth[1]} solid ${colors.grayLines.main}`, "&:nth-child(1)": { diff --git a/src/qqq/pages/apps/Home.tsx b/src/qqq/pages/apps/Home.tsx index bb53faf..c102439 100644 --- a/src/qqq/pages/apps/Home.tsx +++ b/src/qqq/pages/apps/Home.tsx @@ -75,9 +75,17 @@ function AppHome({app}: Props): JSX.Element })(); }, []); + const mdbMetaData = app?.supplementalAppMetaData?.get("materialDashboard"); + let showAppLabelOnHomeScreen = true; + if(mdbMetaData) + { + showAppLabelOnHomeScreen = mdbMetaData.showAppLabelOnHomeScreen; + } + useEffect(() => { - setPageHeader(app.label); + // setPageHeader(app.label); + setPageHeader(null); if (!qInstance) { @@ -208,6 +216,12 @@ function AppHome({app}: Props): JSX.Element { return ( + { + showAppLabelOnHomeScreen && + + {app.label} + + } @@ -253,13 +267,19 @@ function AppHome({app}: Props): JSX.Element return ( + { + showAppLabelOnHomeScreen && + + {app.label} + + } {app.widgets && app.widgets.length > 0 && ( - + )} - + { app.sections ? (