mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-18 13:20:43 +00:00
Merge remote-tracking branch 'origin/dev' into feature/CTLE-434-oms-update-business-logic
# Conflicts: # package.json # src/qqq/utils/HtmlUtils.ts
This commit is contained in:
@ -6,7 +6,7 @@
|
|||||||
"@auth0/auth0-react": "1.10.2",
|
"@auth0/auth0-react": "1.10.2",
|
||||||
"@emotion/react": "11.7.1",
|
"@emotion/react": "11.7.1",
|
||||||
"@emotion/styled": "11.6.0",
|
"@emotion/styled": "11.6.0",
|
||||||
"@kingsrook/qqq-frontend-core": "1.0.62",
|
"@kingsrook/qqq-frontend-core": "1.0.66",
|
||||||
"@mui/icons-material": "5.4.1",
|
"@mui/icons-material": "5.4.1",
|
||||||
"@mui/material": "5.11.1",
|
"@mui/material": "5.11.1",
|
||||||
"@mui/styles": "5.11.1",
|
"@mui/styles": "5.11.1",
|
||||||
@ -30,6 +30,7 @@
|
|||||||
"form-data": "4.0.0",
|
"form-data": "4.0.0",
|
||||||
"formik": "2.2.9",
|
"formik": "2.2.9",
|
||||||
"html-react-parser": "1.4.8",
|
"html-react-parser": "1.4.8",
|
||||||
|
"html-to-text": "^9.0.5",
|
||||||
"http-proxy-middleware": "2.0.6",
|
"http-proxy-middleware": "2.0.6",
|
||||||
"rapidoc": "9.3.4",
|
"rapidoc": "9.3.4",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
|
@ -567,6 +567,7 @@ export default function App()
|
|||||||
icon={branding.icon}
|
icon={branding.icon}
|
||||||
logo={branding.logo}
|
logo={branding.logo}
|
||||||
appName={branding.appName}
|
appName={branding.appName}
|
||||||
|
branding={branding}
|
||||||
routes={sideNavRoutes}
|
routes={sideNavRoutes}
|
||||||
onMouseEnter={handleOnMouseEnter}
|
onMouseEnter={handleOnMouseEnter}
|
||||||
onMouseLeave={handleOnMouseLeave}
|
onMouseLeave={handleOnMouseLeave}
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import {QBrandingMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QBrandingMetaData";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Divider from "@mui/material/Divider";
|
import Divider from "@mui/material/Divider";
|
||||||
import Icon from "@mui/material/Icon";
|
import Icon from "@mui/material/Icon";
|
||||||
@ -42,6 +43,7 @@ interface Props
|
|||||||
icon?: string;
|
icon?: string;
|
||||||
logo?: string;
|
logo?: string;
|
||||||
appName?: string;
|
appName?: string;
|
||||||
|
branding?: QBrandingMetaData;
|
||||||
routes: {
|
routes: {
|
||||||
[key: string]:
|
[key: string]:
|
||||||
| ReactNode
|
| ReactNode
|
||||||
@ -64,7 +66,7 @@ interface Props
|
|||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Sidenav({color, icon, logo, appName, routes, ...rest}: Props): JSX.Element
|
function Sidenav({color, icon, logo, appName, branding, routes, ...rest}: Props): JSX.Element
|
||||||
{
|
{
|
||||||
const [openCollapse, setOpenCollapse] = useState<boolean | string>(false);
|
const [openCollapse, setOpenCollapse] = useState<boolean | string>(false);
|
||||||
const [openNestedCollapse, setOpenNestedCollapse] = useState<boolean | string>(false);
|
const [openNestedCollapse, setOpenNestedCollapse] = useState<boolean | string>(false);
|
||||||
@ -328,6 +330,12 @@ function Sidenav({color, icon, logo, appName, routes, ...rest}: Props): JSX.Elem
|
|||||||
</Box>
|
</Box>
|
||||||
}
|
}
|
||||||
</Box>
|
</Box>
|
||||||
|
{
|
||||||
|
branding && branding.environmentBannerText &&
|
||||||
|
<Box mt={2} bgcolor={branding.environmentBannerColor} borderRadius={2}>
|
||||||
|
{branding.environmentBannerText}
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
</Box>
|
</Box>
|
||||||
<Divider
|
<Divider
|
||||||
light={
|
light={
|
||||||
|
@ -44,10 +44,10 @@ import USMapWidget from "qqq/components/widgets/misc/USMapWidget";
|
|||||||
import ParentWidget from "qqq/components/widgets/ParentWidget";
|
import ParentWidget from "qqq/components/widgets/ParentWidget";
|
||||||
import MultiStatisticsCard from "qqq/components/widgets/statistics/MultiStatisticsCard";
|
import MultiStatisticsCard from "qqq/components/widgets/statistics/MultiStatisticsCard";
|
||||||
import StatisticsCard from "qqq/components/widgets/statistics/StatisticsCard";
|
import StatisticsCard from "qqq/components/widgets/statistics/StatisticsCard";
|
||||||
import TableCard from "qqq/components/widgets/tables/TableCard";
|
|
||||||
import Widget, {WIDGET_DROPDOWN_SELECTION_LOCAL_STORAGE_KEY_ROOT} from "qqq/components/widgets/Widget";
|
import Widget, {WIDGET_DROPDOWN_SELECTION_LOCAL_STORAGE_KEY_ROOT} from "qqq/components/widgets/Widget";
|
||||||
import ProcessRun from "qqq/pages/processes/ProcessRun";
|
import ProcessRun from "qqq/pages/processes/ProcessRun";
|
||||||
import Client from "qqq/utils/qqq/Client";
|
import Client from "qqq/utils/qqq/Client";
|
||||||
|
import TableWidget from "./tables/TableWidget";
|
||||||
|
|
||||||
|
|
||||||
const qController = Client.getInstance();
|
const qController = Client.getInstance();
|
||||||
@ -96,10 +96,26 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
|||||||
|
|
||||||
widgetData[i] = {};
|
widgetData[i] = {};
|
||||||
(async () =>
|
(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
widgetData[i] = await qController.widget(widgetMetaData.name, urlParams);
|
widgetData[i] = await qController.widget(widgetMetaData.name, urlParams);
|
||||||
setWidgetData(widgetData);
|
setWidgetData(widgetData);
|
||||||
setWidgetCounter(widgetCounter + 1);
|
setWidgetCounter(widgetCounter + 1);
|
||||||
|
if(widgetData[i])
|
||||||
|
{
|
||||||
|
widgetData[i]["errorLoading"] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(e)
|
||||||
|
{
|
||||||
|
console.error(e);
|
||||||
|
if(widgetData[i])
|
||||||
|
{
|
||||||
|
widgetData[i]["errorLoading"] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
forceUpdate();
|
forceUpdate();
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
@ -111,13 +127,31 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
|||||||
{
|
{
|
||||||
const urlParams = getQueryParams(widgetMetaDataList[index], data);
|
const urlParams = getQueryParams(widgetMetaDataList[index], data);
|
||||||
setCurrentUrlParams(urlParams);
|
setCurrentUrlParams(urlParams);
|
||||||
|
widgetData[index] = {};
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
widgetData[index] = await qController.widget(widgetMetaDataList[index].name, urlParams);
|
widgetData[index] = await qController.widget(widgetMetaDataList[index].name, urlParams);
|
||||||
setWidgetCounter(widgetCounter + 1);
|
setWidgetCounter(widgetCounter + 1);
|
||||||
setWidgetData(widgetData);
|
setWidgetData(widgetData);
|
||||||
|
|
||||||
|
if (widgetData[index])
|
||||||
|
{
|
||||||
|
widgetData[index]["errorLoading"] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(e)
|
||||||
|
{
|
||||||
|
console.error(e);
|
||||||
|
if (widgetData[index])
|
||||||
|
{
|
||||||
|
widgetData[index]["errorLoading"] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
forceUpdate();
|
forceUpdate();
|
||||||
})();
|
})();
|
||||||
};
|
}
|
||||||
|
|
||||||
function getQueryParams(widgetMetaData: QWidgetMetaData, extraParams: string): string
|
function getQueryParams(widgetMetaData: QWidgetMetaData, extraParams: string): string
|
||||||
{
|
{
|
||||||
@ -221,20 +255,12 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
|||||||
}
|
}
|
||||||
{
|
{
|
||||||
widgetMetaData.type === "table" && (
|
widgetMetaData.type === "table" && (
|
||||||
<Widget
|
<TableWidget
|
||||||
widgetMetaData={widgetMetaData}
|
widgetMetaData={widgetMetaData}
|
||||||
widgetData={widgetData[i]}
|
widgetData={widgetData[i]}
|
||||||
reloadWidgetCallback={(data) => reloadWidget(i, data)}
|
reloadWidgetCallback={(data) => reloadWidget(i, data)}
|
||||||
footerHTML={widgetData[i]?.footerHTML}
|
|
||||||
isChild={areChildren}
|
isChild={areChildren}
|
||||||
>
|
|
||||||
<TableCard
|
|
||||||
noRowsFoundHTML={widgetData[i]?.noRowsFoundHTML}
|
|
||||||
rowsPerPage={widgetData[i]?.rowsPerPage}
|
|
||||||
hidePaginationDropdown={widgetData[i]?.hidePaginationDropdown}
|
|
||||||
data={widgetData[i]}
|
|
||||||
/>
|
/>
|
||||||
</Widget>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
@ -254,7 +280,9 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
|||||||
<Widget
|
<Widget
|
||||||
widgetMetaData={widgetMetaData}
|
widgetMetaData={widgetMetaData}
|
||||||
widgetData={widgetData[i]}
|
widgetData={widgetData[i]}
|
||||||
reloadWidgetCallback={(data) => reloadWidget(i, data)}>
|
reloadWidgetCallback={(data) => reloadWidget(i, data)}
|
||||||
|
showReloadControl={false}
|
||||||
|
>
|
||||||
<div className="widgetProcessMidDiv" style={{height: "100%"}}>
|
<div className="widgetProcessMidDiv" style={{height: "100%"}}>
|
||||||
<ProcessRun process={widgetData[i]?.processMetaData} defaultProcessValues={widgetData[i]?.defaultValues} isWidget={true} forceReInit={widgetCounter} />
|
<ProcessRun process={widgetData[i]?.processMetaData} defaultProcessValues={widgetData[i]?.defaultValues} isWidget={true} forceReInit={widgetCounter} />
|
||||||
</div>
|
</div>
|
||||||
@ -265,7 +293,9 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
|||||||
widgetMetaData.type === "stepper" && (
|
widgetMetaData.type === "stepper" && (
|
||||||
<Widget
|
<Widget
|
||||||
widgetMetaData={widgetMetaData}
|
widgetMetaData={widgetMetaData}
|
||||||
widgetData={widgetData[i]}>
|
widgetData={widgetData[i]}
|
||||||
|
reloadWidgetCallback={(data) => reloadWidget(i, data)}
|
||||||
|
>
|
||||||
<Box sx={{alignItems: "stretch", flexGrow: 1, display: "flex", marginTop: "0px", paddingTop: "0px"}}>
|
<Box sx={{alignItems: "stretch", flexGrow: 1, display: "flex", marginTop: "0px", paddingTop: "0px"}}>
|
||||||
<Box padding="1rem" sx={{width: "100%"}}>
|
<Box padding="1rem" sx={{width: "100%"}}>
|
||||||
<StepperCard data={widgetData[i]} />
|
<StepperCard data={widgetData[i]} />
|
||||||
@ -276,7 +306,11 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
|||||||
}
|
}
|
||||||
{
|
{
|
||||||
widgetMetaData.type === "html" && (
|
widgetMetaData.type === "html" && (
|
||||||
<Widget widgetMetaData={widgetMetaData}>
|
<Widget
|
||||||
|
widgetMetaData={widgetMetaData}
|
||||||
|
reloadWidgetCallback={(data) => reloadWidget(i, data)}
|
||||||
|
widgetData={widgetData[i]}
|
||||||
|
>
|
||||||
<Box px={3} pt={0} pb={2}>
|
<Box px={3} pt={0} pb={2}>
|
||||||
<MDTypography component="div" variant="button" color="text" fontWeight="light">
|
<MDTypography component="div" variant="button" color="text" fontWeight="light">
|
||||||
{
|
{
|
||||||
@ -306,8 +340,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
|||||||
widgetMetaData={widgetMetaData}
|
widgetMetaData={widgetMetaData}
|
||||||
widgetData={widgetData[i]}
|
widgetData={widgetData[i]}
|
||||||
isChild={areChildren}
|
isChild={areChildren}
|
||||||
|
reloadWidgetCallback={(data) => reloadWidget(i, data)}
|
||||||
// reloadWidgetCallback={(data) => reloadWidget(i, data)}
|
|
||||||
>
|
>
|
||||||
<StatisticsCard
|
<StatisticsCard
|
||||||
data={widgetData[i]}
|
data={widgetData[i]}
|
||||||
@ -346,6 +379,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
|||||||
<Widget
|
<Widget
|
||||||
widgetMetaData={widgetMetaData}
|
widgetMetaData={widgetMetaData}
|
||||||
widgetData={widgetData[i]}
|
widgetData={widgetData[i]}
|
||||||
|
reloadWidgetCallback={(data) => reloadWidget(i, data)}
|
||||||
isChild={areChildren}
|
isChild={areChildren}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
@ -379,7 +413,9 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
|||||||
<Widget
|
<Widget
|
||||||
widgetMetaData={widgetMetaData}
|
widgetMetaData={widgetMetaData}
|
||||||
widgetData={widgetData[i]}
|
widgetData={widgetData[i]}
|
||||||
isChild={areChildren}>
|
reloadWidgetCallback={(data) => reloadWidget(i, data)}
|
||||||
|
isChild={areChildren}
|
||||||
|
>
|
||||||
<DefaultLineChart sx={{alignItems: "center"}}
|
<DefaultLineChart sx={{alignItems: "center"}}
|
||||||
data={widgetData[i]?.chartData}
|
data={widgetData[i]?.chartData}
|
||||||
isYAxisCurrency={widgetData[i]?.isYAxisCurrency}
|
isYAxisCurrency={widgetData[i]?.isYAxisCurrency}
|
||||||
|
@ -25,10 +25,13 @@ import Box from "@mui/material/Box";
|
|||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
import Card from "@mui/material/Card";
|
import Card from "@mui/material/Card";
|
||||||
import Icon from "@mui/material/Icon";
|
import Icon from "@mui/material/Icon";
|
||||||
|
import LinearProgress from "@mui/material/LinearProgress";
|
||||||
|
import Tooltip from "@mui/material/Tooltip/Tooltip";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import parse from "html-react-parser";
|
import parse from "html-react-parser";
|
||||||
import React, {useState} from "react";
|
import React, {useEffect, useState} from "react";
|
||||||
import {Link, useNavigate} from "react-router-dom";
|
import {Link, useNavigate, NavigateFunction} from "react-router-dom";
|
||||||
|
import {bool} from "yup";
|
||||||
import colors from "qqq/components/legacy/colors";
|
import colors from "qqq/components/legacy/colors";
|
||||||
import DropdownMenu, {DropdownOption} from "qqq/components/widgets/components/DropdownMenu";
|
import DropdownMenu, {DropdownOption} from "qqq/components/widgets/components/DropdownMenu";
|
||||||
|
|
||||||
@ -43,6 +46,8 @@ export interface WidgetData
|
|||||||
}[][];
|
}[][];
|
||||||
dropdownNeedsSelectedText?: string;
|
dropdownNeedsSelectedText?: string;
|
||||||
hasPermission?: boolean;
|
hasPermission?: boolean;
|
||||||
|
errorLoading?: boolean;
|
||||||
|
[other: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -54,6 +59,7 @@ interface Props
|
|||||||
widgetData?: WidgetData;
|
widgetData?: WidgetData;
|
||||||
children: JSX.Element;
|
children: JSX.Element;
|
||||||
reloadWidgetCallback?: (params: string) => void;
|
reloadWidgetCallback?: (params: string) => void;
|
||||||
|
showReloadControl: boolean;
|
||||||
isChild?: boolean;
|
isChild?: boolean;
|
||||||
footerHTML?: string;
|
footerHTML?: string;
|
||||||
storeDropdownSelections?: boolean;
|
storeDropdownSelections?: boolean;
|
||||||
@ -61,6 +67,7 @@ interface Props
|
|||||||
|
|
||||||
Widget.defaultProps = {
|
Widget.defaultProps = {
|
||||||
isChild: false,
|
isChild: false,
|
||||||
|
showReloadControl: true,
|
||||||
widgetMetaData: {},
|
widgetMetaData: {},
|
||||||
widgetData: {},
|
widgetData: {},
|
||||||
labelAdditionalComponentsLeft: [],
|
labelAdditionalComponentsLeft: [],
|
||||||
@ -68,9 +75,22 @@ Widget.defaultProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
interface LabelComponentRenderArgs
|
||||||
|
{
|
||||||
|
navigate: NavigateFunction;
|
||||||
|
widgetProps: Props;
|
||||||
|
dropdownData: any[];
|
||||||
|
componentIndex: number;
|
||||||
|
reloadFunction: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export class LabelComponent
|
export class LabelComponent
|
||||||
{
|
{
|
||||||
|
render = (args: LabelComponentRenderArgs): JSX.Element =>
|
||||||
|
{
|
||||||
|
return (<div>Unsupported component type</div>)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -86,6 +106,15 @@ export class HeaderLink extends LabelComponent
|
|||||||
this.label = label;
|
this.label = label;
|
||||||
this.to = to;
|
this.to = to;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
render = (args: LabelComponentRenderArgs): JSX.Element =>
|
||||||
|
{
|
||||||
|
return (
|
||||||
|
<Typography variant="body2" p={2} display="inline" fontSize=".875rem" pt="0" position="relative" top="-0.25rem">
|
||||||
|
{this.to ? <Link to={this.to}>{this.label}</Link> : null}
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -97,6 +126,7 @@ export class AddNewRecordButton extends LabelComponent
|
|||||||
defaultValues: any;
|
defaultValues: any;
|
||||||
disabledFields: any;
|
disabledFields: any;
|
||||||
|
|
||||||
|
|
||||||
constructor(table: QTableMetaData, defaultValues: any, label: string = "Add new", disabledFields: any = defaultValues)
|
constructor(table: QTableMetaData, defaultValues: any, label: string = "Add new", disabledFields: any = defaultValues)
|
||||||
{
|
{
|
||||||
super();
|
super();
|
||||||
@ -105,6 +135,45 @@ export class AddNewRecordButton extends LabelComponent
|
|||||||
this.defaultValues = defaultValues;
|
this.defaultValues = defaultValues;
|
||||||
this.disabledFields = disabledFields;
|
this.disabledFields = disabledFields;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
openEditForm = (navigate: any, table: QTableMetaData, id: any = null, defaultValues: any, disabledFields: any) =>
|
||||||
|
{
|
||||||
|
navigate(`#/createChild=${table.name}/defaultValues=${JSON.stringify(defaultValues)}/disabledFields=${JSON.stringify(disabledFields)}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
render = (args: LabelComponentRenderArgs): JSX.Element =>
|
||||||
|
{
|
||||||
|
return (
|
||||||
|
<Typography variant="body2" p={2} pr={0} display="inline" position="relative" top="0.25rem">
|
||||||
|
<Button sx={{mt: 0.75}} onClick={() => this.openEditForm(args.navigate, this.table, null, this.defaultValues, this.disabledFields)}>{this.label}</Button>
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export class ExportDataButton extends LabelComponent
|
||||||
|
{
|
||||||
|
callbackToExport: any;
|
||||||
|
tooltipTitle: string;
|
||||||
|
isDisabled: boolean;
|
||||||
|
|
||||||
|
constructor(callbackToExport: any, isDisabled = false, tooltipTitle: string = "Export")
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
this.callbackToExport = callbackToExport;
|
||||||
|
this.isDisabled = isDisabled;
|
||||||
|
this.tooltipTitle = tooltipTitle;
|
||||||
|
}
|
||||||
|
|
||||||
|
render = (args: LabelComponentRenderArgs): JSX.Element =>
|
||||||
|
{
|
||||||
|
return (
|
||||||
|
<Typography variant="body2" py={2} px={0} display="inline" position="relative" top="-0.375rem">
|
||||||
|
<Tooltip title={this.tooltipTitle}><Button sx={{px: 1, py: 0, minWidth: "initial"}} onClick={() => this.callbackToExport()} disabled={this.isDisabled}><Icon>save_alt</Icon></Button></Tooltip>
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -121,6 +190,55 @@ export class Dropdown extends LabelComponent
|
|||||||
this.options = options;
|
this.options = options;
|
||||||
this.onChangeCallback = onChangeCallback;
|
this.onChangeCallback = onChangeCallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
render = (args: LabelComponentRenderArgs): JSX.Element =>
|
||||||
|
{
|
||||||
|
let defaultValue = null;
|
||||||
|
const dropdownName = args.widgetProps.widgetData.dropdownNameList[args.componentIndex];
|
||||||
|
const localStorageKey = `${WIDGET_DROPDOWN_SELECTION_LOCAL_STORAGE_KEY_ROOT}.${args.widgetProps.widgetMetaData.name}.${dropdownName}`;
|
||||||
|
if(args.widgetProps.storeDropdownSelections)
|
||||||
|
{
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// see if an existing value is stored in local storage, and if so set it in dropdown //
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
defaultValue = JSON.parse(localStorage.getItem(localStorageKey));
|
||||||
|
args.dropdownData[args.componentIndex] = defaultValue?.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box my={2} sx={{float: "right"}}>
|
||||||
|
<DropdownMenu
|
||||||
|
name={dropdownName}
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
sx={{width: 200, marginLeft: "15px"}}
|
||||||
|
label={`Select ${this.label}`}
|
||||||
|
dropdownOptions={this.options}
|
||||||
|
onChangeCallback={this.onChangeCallback}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export class ReloadControl extends LabelComponent
|
||||||
|
{
|
||||||
|
callback: () => void;
|
||||||
|
|
||||||
|
constructor(callback: () => void)
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
this.callback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
render = (args: LabelComponentRenderArgs): JSX.Element =>
|
||||||
|
{
|
||||||
|
return (
|
||||||
|
<Typography variant="body2" py={2} px={0} display="inline" position="relative" top="-0.375rem">
|
||||||
|
<Tooltip title="Refresh"><Button sx={{px: 1, py:0, minWidth: "initial"}} onClick={() => this.callback()}><Icon>refresh</Icon></Button></Tooltip>
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -132,64 +250,11 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [dropdownData, setDropdownData] = useState([]);
|
const [dropdownData, setDropdownData] = useState([]);
|
||||||
const [fullScreenWidgetClassName, setFullScreenWidgetClassName] = useState("");
|
const [fullScreenWidgetClassName, setFullScreenWidgetClassName] = useState("");
|
||||||
|
const [reloading, setReloading] = useState(false);
|
||||||
|
|
||||||
function openEditForm(table: QTableMetaData, id: any = null, defaultValues: any, disabledFields: any)
|
function renderComponent(component: LabelComponent, componentIndex: number)
|
||||||
{
|
{
|
||||||
navigate(`#/createChild=${table.name}/defaultValues=${JSON.stringify(defaultValues)}/disabledFields=${JSON.stringify(disabledFields)}`)
|
return component.render({navigate: navigate, widgetProps: props, dropdownData: dropdownData, componentIndex: componentIndex, reloadFunction: doReload})
|
||||||
}
|
|
||||||
|
|
||||||
function renderComponent(component: LabelComponent, index: number)
|
|
||||||
{
|
|
||||||
if(component instanceof HeaderLink)
|
|
||||||
{
|
|
||||||
const link = component as HeaderLink
|
|
||||||
return (
|
|
||||||
<Typography variant="body2" p={2} display="inline" fontSize=".875rem" pt="0" position="relative" top="-0.25rem">
|
|
||||||
{link.to ? <Link to={link.to}>{link.label}</Link> : null}
|
|
||||||
</Typography>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (component instanceof AddNewRecordButton)
|
|
||||||
{
|
|
||||||
const addNewRecordButton = component as AddNewRecordButton
|
|
||||||
return (
|
|
||||||
<Typography variant="body2" p={2} pr={0} display="inline" position="relative" top="0.25rem">
|
|
||||||
<Button sx={{mt: 0.75}} onClick={() => openEditForm(addNewRecordButton.table, null, addNewRecordButton.defaultValues, addNewRecordButton.disabledFields)}>{addNewRecordButton.label}</Button>
|
|
||||||
</Typography>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (component instanceof Dropdown)
|
|
||||||
{
|
|
||||||
let defaultValue = null;
|
|
||||||
const dropdownName = props.widgetData.dropdownNameList[index];
|
|
||||||
const localStorageKey = `${WIDGET_DROPDOWN_SELECTION_LOCAL_STORAGE_KEY_ROOT}.${props.widgetMetaData.name}.${dropdownName}`;
|
|
||||||
if(props.storeDropdownSelections)
|
|
||||||
{
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// see if an existing value is stored in local storage, and if so set it in dropdown //
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
defaultValue = JSON.parse(localStorage.getItem(localStorageKey));
|
|
||||||
dropdownData[index] = defaultValue?.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
const dropdown = component as Dropdown
|
|
||||||
return (
|
|
||||||
<Box my={2} sx={{float: "right"}}>
|
|
||||||
<DropdownMenu
|
|
||||||
name={dropdownName}
|
|
||||||
defaultValue={defaultValue}
|
|
||||||
sx={{width: 200, marginLeft: "15px"}}
|
|
||||||
label={`Select ${dropdown.label}`}
|
|
||||||
dropdownOptions={dropdown.options}
|
|
||||||
onChangeCallback={dropdown.onChangeCallback}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (<div>Unsupported component type.</div>)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -209,6 +274,26 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const doReload = () =>
|
||||||
|
{
|
||||||
|
setReloading(true);
|
||||||
|
reloadWidget(dropdownData);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() =>
|
||||||
|
{
|
||||||
|
setReloading(false);
|
||||||
|
}, [props.widgetData]);
|
||||||
|
|
||||||
|
const effectiveLabelAdditionalComponentsLeft: LabelComponent[] = [];
|
||||||
|
if(props.reloadWidgetCallback && props.widgetData && props.showReloadControl && props.widgetMetaData.showReloadButton)
|
||||||
|
{
|
||||||
|
effectiveLabelAdditionalComponentsLeft.push(new ReloadControl(doReload))
|
||||||
|
}
|
||||||
|
if(props.labelAdditionalComponentsLeft)
|
||||||
|
{
|
||||||
|
props.labelAdditionalComponentsLeft.map((component) => effectiveLabelAdditionalComponentsLeft.push(component));
|
||||||
|
}
|
||||||
|
|
||||||
function handleDataChange(dropdownLabel: string, changedData: any)
|
function handleDataChange(dropdownLabel: string, changedData: any)
|
||||||
{
|
{
|
||||||
@ -297,9 +382,31 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
|||||||
}
|
}
|
||||||
|
|
||||||
const hasPermission = props.widgetData?.hasPermission === undefined || props.widgetData?.hasPermission === true;
|
const hasPermission = props.widgetData?.hasPermission === undefined || props.widgetData?.hasPermission === true;
|
||||||
|
|
||||||
|
const isSet = (v: any): boolean =>
|
||||||
|
{
|
||||||
|
return(v !== null && v !== undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// to avoid taking up the space of the Box with the label and icon and label-components (since it has a height), only output that box if we need any of the components //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
let needLabelBox = false;
|
||||||
|
if(hasPermission)
|
||||||
|
{
|
||||||
|
needLabelBox ||= (effectiveLabelAdditionalComponentsLeft && effectiveLabelAdditionalComponentsLeft.length > 0);
|
||||||
|
needLabelBox ||= (effectiveLabelAdditionalComponentsRight && effectiveLabelAdditionalComponentsRight.length > 0);
|
||||||
|
needLabelBox ||= isSet(props.widgetMetaData?.icon);
|
||||||
|
needLabelBox ||= isSet(props.widgetData?.label);
|
||||||
|
needLabelBox ||= isSet(props.widgetMetaData?.label);
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorLoading = props.widgetData?.errorLoading !== undefined && props.widgetData?.errorLoading === true;
|
||||||
const widgetContent =
|
const widgetContent =
|
||||||
<Box sx={{width: "100%", height: "100%", minHeight: props.widgetMetaData?.minHeight ? props.widgetMetaData?.minHeight : "initial"}}>
|
<Box sx={{width: "100%", height: "100%", minHeight: props.widgetMetaData?.minHeight ? props.widgetMetaData?.minHeight : "initial"}}>
|
||||||
<Box pr={2} display="flex" justifyContent="space-between" alignItems="flex-start" sx={{width: "100%"}}>
|
{
|
||||||
|
needLabelBox &&
|
||||||
|
<Box pr={2} display="flex" justifyContent="space-between" alignItems="flex-start" sx={{width: "100%"}} height={"3.5rem"}>
|
||||||
<Box pt={2} pb={1}>
|
<Box pt={2} pb={1}>
|
||||||
{
|
{
|
||||||
hasPermission ?
|
hasPermission ?
|
||||||
@ -360,14 +467,9 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
{/*
|
|
||||||
<Button onClick={() => toggleFullScreenWidget()}>
|
|
||||||
{fullScreenWidgetClassName ? "-" : "+"}
|
|
||||||
</Button>
|
|
||||||
*/}
|
|
||||||
{
|
{
|
||||||
hasPermission && (
|
hasPermission && (
|
||||||
props.labelAdditionalComponentsLeft.map((component, i) =>
|
effectiveLabelAdditionalComponentsLeft.map((component, i) =>
|
||||||
{
|
{
|
||||||
return (<span key={i}>{renderComponent(component, i)}</span>);
|
return (<span key={i}>{renderComponent(component, i)}</span>);
|
||||||
})
|
})
|
||||||
@ -385,7 +487,17 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
|||||||
}
|
}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
}
|
||||||
{
|
{
|
||||||
|
props.widgetMetaData?.isCard && (reloading ? <LinearProgress color="info" sx={{overflow: "hidden", borderRadius: "0"}} /> : <Box height="0.375rem"/>)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
errorLoading ? (
|
||||||
|
<Box pb={3} sx={{display: "flex", justifyContent: "center", alignItems: "flex-start"}}>
|
||||||
|
<Icon color="error">error</Icon>
|
||||||
|
<Typography sx={{paddingLeft: "4px", textTransform: "revert"}} variant="button">An error occurred loading widget content.</Typography>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
hasPermission && props.widgetData?.dropdownNeedsSelectedText ? (
|
hasPermission && props.widgetData?.dropdownNeedsSelectedText ? (
|
||||||
<Box pb={3} pr={3} sx={{width: "100%", textAlign: "right"}}>
|
<Box pb={3} pr={3} sx={{width: "100%", textAlign: "right"}}>
|
||||||
<Typography variant="body2">
|
<Typography variant="body2">
|
||||||
@ -399,15 +511,20 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
|||||||
<Box mt={2} mb={5} sx={{display: "flex", justifyContent: "center"}}><Typography variant="body2">You do not have permission to view this data.</Typography></Box>
|
<Box mt={2} mb={5} sx={{display: "flex", justifyContent: "center"}}><Typography variant="body2">You do not have permission to view this data.</Typography></Box>
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
props?.footerHTML && (
|
! errorLoading && props?.footerHTML && (
|
||||||
<Box mt={1} ml={3} mr={3} mb={2} sx={{fontWeight: 300, color: "#7b809a", display: "flex", alignContent: "flex-end", fontSize: "14px"}}>{parse(props.footerHTML)}</Box>
|
<Box mt={1} ml={3} mr={3} mb={2} sx={{fontWeight: 300, color: "#7b809a", display: "flex", alignContent: "flex-end", fontSize: "14px"}}>{parse(props.footerHTML)}</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
</Box>;
|
</Box>;
|
||||||
|
|
||||||
return props.widgetMetaData?.isCard ? <Card sx={{marginTop: props.widgetMetaData?.icon ? 2 : 0, width: "100%"}} className={fullScreenWidgetClassName}>{widgetContent}</Card> : widgetContent;
|
return props.widgetMetaData?.isCard
|
||||||
|
? <Card sx={{marginTop: props.widgetMetaData?.icon ? 2 : 0, width: "100%"}} className={fullScreenWidgetClassName}>
|
||||||
|
{widgetContent}
|
||||||
|
</Card>
|
||||||
|
: widgetContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Widget;
|
export default Widget;
|
||||||
|
@ -25,9 +25,11 @@ import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
|||||||
import {DataGridPro, GridCallbackDetails, GridRowParams, MuiEvent} from "@mui/x-data-grid-pro";
|
import {DataGridPro, GridCallbackDetails, GridRowParams, MuiEvent} from "@mui/x-data-grid-pro";
|
||||||
import React, {useEffect, useState} from "react";
|
import React, {useEffect, useState} from "react";
|
||||||
import {useNavigate} from "react-router-dom";
|
import {useNavigate} from "react-router-dom";
|
||||||
import Widget, {AddNewRecordButton, HeaderLink, LabelComponent} from "qqq/components/widgets/Widget";
|
import Widget, {AddNewRecordButton, ExportDataButton, HeaderLink, LabelComponent} from "qqq/components/widgets/Widget";
|
||||||
import DataGridUtils from "qqq/utils/DataGridUtils";
|
import DataGridUtils from "qqq/utils/DataGridUtils";
|
||||||
|
import HtmlUtils from "qqq/utils/HtmlUtils";
|
||||||
import Client from "qqq/utils/qqq/Client";
|
import Client from "qqq/utils/qqq/Client";
|
||||||
|
import ValueUtils from "qqq/utils/qqq/ValueUtils";
|
||||||
|
|
||||||
interface Props
|
interface Props
|
||||||
{
|
{
|
||||||
@ -42,7 +44,9 @@ const qController = Client.getInstance();
|
|||||||
function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element
|
function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element
|
||||||
{
|
{
|
||||||
const [rows, setRows] = useState([]);
|
const [rows, setRows] = useState([]);
|
||||||
|
const [records, setRecords] = useState([] as QRecord[])
|
||||||
const [columns, setColumns] = useState([]);
|
const [columns, setColumns] = useState([]);
|
||||||
|
const [allColumns, setAllColumns] = useState([])
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
@ -68,6 +72,11 @@ function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element
|
|||||||
const childTablePath = data.tablePath ? data.tablePath + (data.tablePath.endsWith("/") ? "" : "/") : data.tablePath;
|
const childTablePath = data.tablePath ? data.tablePath + (data.tablePath.endsWith("/") ? "" : "/") : data.tablePath;
|
||||||
const columns = DataGridUtils.setupGridColumns(tableMetaData, childTablePath, null, "bySection");
|
const columns = DataGridUtils.setupGridColumns(tableMetaData, childTablePath, null, "bySection");
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// capture all-columns to use for the export (before we might splice some away from the on-screen display) //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
setAllColumns(JSON.parse(JSON.stringify(columns)));
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////
|
||||||
// do not not show the foreign-key column of the parent table //
|
// do not not show the foreign-key column of the parent table //
|
||||||
////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////
|
||||||
@ -84,16 +93,67 @@ function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element
|
|||||||
}
|
}
|
||||||
|
|
||||||
setRows(rows);
|
setRows(rows);
|
||||||
|
setRecords(records)
|
||||||
setColumns(columns);
|
setColumns(columns);
|
||||||
}
|
}
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
|
const exportCallback = () =>
|
||||||
|
{
|
||||||
|
let csv = "";
|
||||||
|
for (let i = 0; i < allColumns.length; i++)
|
||||||
|
{
|
||||||
|
csv += `${i > 0 ? "," : ""}"${ValueUtils.cleanForCsv(allColumns[i].headerName)}"`
|
||||||
|
}
|
||||||
|
csv += "\n";
|
||||||
|
|
||||||
|
for (let i = 0; i < records.length; i++)
|
||||||
|
{
|
||||||
|
for (let j = 0; j < allColumns.length; j++)
|
||||||
|
{
|
||||||
|
const value = records[i].displayValues.get(allColumns[j].field) ?? records[i].values.get(allColumns[j].field)
|
||||||
|
csv += `${j > 0 ? "," : ""}"${ValueUtils.cleanForCsv(value)}"`
|
||||||
|
}
|
||||||
|
csv += "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileName = (data?.label ?? widgetMetaData.label) + " " + ValueUtils.formatDateTimeForFileName(new Date()) + ".csv";
|
||||||
|
HtmlUtils.download(fileName, csv);
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////
|
||||||
|
// view all link //
|
||||||
|
///////////////////
|
||||||
const labelAdditionalComponentsLeft: LabelComponent[] = []
|
const labelAdditionalComponentsLeft: LabelComponent[] = []
|
||||||
if(data && data.viewAllLink)
|
if(data && data.viewAllLink)
|
||||||
{
|
{
|
||||||
labelAdditionalComponentsLeft.push(new HeaderLink("View All", data.viewAllLink));
|
labelAdditionalComponentsLeft.push(new HeaderLink("View All", data.viewAllLink));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///////////////////
|
||||||
|
// export button //
|
||||||
|
///////////////////
|
||||||
|
let isExportDisabled = true;
|
||||||
|
let tooltipTitle = "Export";
|
||||||
|
if (data && data.childTableMetaData && data.queryOutput && data.queryOutput.records && data.queryOutput.records.length > 0)
|
||||||
|
{
|
||||||
|
isExportDisabled = false;
|
||||||
|
|
||||||
|
if(data.totalRows && data.queryOutput.records.length < data.totalRows)
|
||||||
|
{
|
||||||
|
tooltipTitle = "Export these " + data.queryOutput.records.length + " records."
|
||||||
|
if(data.viewAllLink)
|
||||||
|
{
|
||||||
|
tooltipTitle += "\nClick View All to export all records.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
labelAdditionalComponentsLeft.push(new ExportDataButton(() => exportCallback(), isExportDisabled, tooltipTitle))
|
||||||
|
|
||||||
|
////////////////////
|
||||||
|
// add new button //
|
||||||
|
////////////////////
|
||||||
const labelAdditionalComponentsRight: LabelComponent[] = []
|
const labelAdditionalComponentsRight: LabelComponent[] = []
|
||||||
if(data && data.canAddChildRecord)
|
if(data && data.canAddChildRecord)
|
||||||
{
|
{
|
||||||
@ -123,6 +183,7 @@ function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element
|
|||||||
return (
|
return (
|
||||||
<Widget
|
<Widget
|
||||||
widgetMetaData={widgetMetaData}
|
widgetMetaData={widgetMetaData}
|
||||||
|
widgetData={data}
|
||||||
labelAdditionalComponentsLeft={labelAdditionalComponentsLeft}
|
labelAdditionalComponentsLeft={labelAdditionalComponentsLeft}
|
||||||
labelAdditionalComponentsRight={labelAdditionalComponentsRight}
|
labelAdditionalComponentsRight={labelAdditionalComponentsRight}
|
||||||
>
|
>
|
||||||
|
131
src/qqq/components/widgets/tables/TableWidget.tsx
Normal file
131
src/qqq/components/widgets/tables/TableWidget.tsx
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
/*
|
||||||
|
* 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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
||||||
|
// @ts-ignore
|
||||||
|
import {htmlToText} from "html-to-text";
|
||||||
|
import React, {useEffect, useState} from "react";
|
||||||
|
import TableCard from "qqq/components/widgets/tables/TableCard";
|
||||||
|
import Widget, {ExportDataButton, WidgetData} from "qqq/components/widgets/Widget";
|
||||||
|
import HtmlUtils from "qqq/utils/HtmlUtils";
|
||||||
|
import ValueUtils from "qqq/utils/qqq/ValueUtils";
|
||||||
|
|
||||||
|
interface Props
|
||||||
|
{
|
||||||
|
widgetMetaData?: QWidgetMetaData;
|
||||||
|
widgetData?: WidgetData;
|
||||||
|
reloadWidgetCallback?: (params: string) => void;
|
||||||
|
isChild?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
TableWidget.defaultProps = {
|
||||||
|
};
|
||||||
|
|
||||||
|
function TableWidget(props: Props): JSX.Element
|
||||||
|
{
|
||||||
|
const [isExportDisabled, setIsExportDisabled] = useState(true);
|
||||||
|
|
||||||
|
const rows = props.widgetData?.rows;
|
||||||
|
const columns = props.widgetData?.columns;
|
||||||
|
|
||||||
|
useEffect(() =>
|
||||||
|
{
|
||||||
|
let isExportDisabled = true;
|
||||||
|
if (props.widgetData && columns && rows && rows.length > 0)
|
||||||
|
{
|
||||||
|
isExportDisabled = false;
|
||||||
|
}
|
||||||
|
setIsExportDisabled(isExportDisabled);
|
||||||
|
|
||||||
|
}, [props.widgetMetaData, props.widgetData]);
|
||||||
|
|
||||||
|
const exportCallback = () =>
|
||||||
|
{
|
||||||
|
if (props.widgetData && rows && columns)
|
||||||
|
{
|
||||||
|
console.log(props.widgetData);
|
||||||
|
|
||||||
|
let csv = "";
|
||||||
|
for (let j = 0; j < columns.length; j++)
|
||||||
|
{
|
||||||
|
if (j > 0)
|
||||||
|
{
|
||||||
|
csv += ",";
|
||||||
|
}
|
||||||
|
csv += `"${columns[j].header}"`;
|
||||||
|
}
|
||||||
|
csv += "\n";
|
||||||
|
|
||||||
|
for (let i = 0; i < rows.length; i++)
|
||||||
|
{
|
||||||
|
for (let j = 0; j < columns.length; j++)
|
||||||
|
{
|
||||||
|
if (j > 0)
|
||||||
|
{
|
||||||
|
csv += ",";
|
||||||
|
}
|
||||||
|
|
||||||
|
const cell = rows[i][columns[j].accessor];
|
||||||
|
const text = htmlToText(cell,
|
||||||
|
{
|
||||||
|
selectors: [
|
||||||
|
{selector: "a", format: "inline"},
|
||||||
|
{selector: ".MuiIcon-root", format: "skip"},
|
||||||
|
{selector: ".button", format: "skip"}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
csv += `"${ValueUtils.cleanForCsv(text)}"`;
|
||||||
|
}
|
||||||
|
csv += "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(csv);
|
||||||
|
|
||||||
|
const fileName = (props.widgetData.label ?? props.widgetMetaData.label) + " " + ValueUtils.formatDateTimeForFileName(new Date()) + ".csv";
|
||||||
|
HtmlUtils.download(fileName, csv);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
alert("There is no data available to export.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Widget
|
||||||
|
widgetMetaData={props.widgetMetaData}
|
||||||
|
widgetData={props.widgetData}
|
||||||
|
reloadWidgetCallback={(data) => props.reloadWidgetCallback(data)}
|
||||||
|
footerHTML={props.widgetData?.footerHTML}
|
||||||
|
isChild={props.isChild}
|
||||||
|
labelAdditionalComponentsLeft={props.widgetMetaData?.showExportButton ? [new ExportDataButton(() => exportCallback(), isExportDisabled)] : []}
|
||||||
|
>
|
||||||
|
<TableCard
|
||||||
|
noRowsFoundHTML={props.widgetData?.noRowsFoundHTML}
|
||||||
|
rowsPerPage={props.widgetData?.rowsPerPage}
|
||||||
|
hidePaginationDropdown={props.widgetData?.hidePaginationDropdown}
|
||||||
|
data={{columns: props.widgetData?.columns, rows: props.widgetData?.rows}}
|
||||||
|
/>
|
||||||
|
</Widget>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TableWidget;
|
@ -26,7 +26,8 @@ import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/
|
|||||||
import {QReportMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QReportMetaData";
|
import {QReportMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QReportMetaData";
|
||||||
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
||||||
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
||||||
import {Box, Icon, Typography} from "@mui/material";
|
import {Icon, Typography} from "@mui/material";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
import Card from "@mui/material/Card";
|
import Card from "@mui/material/Card";
|
||||||
import Divider from "@mui/material/Divider";
|
import Divider from "@mui/material/Divider";
|
||||||
import Grid from "@mui/material/Grid";
|
import Grid from "@mui/material/Grid";
|
||||||
@ -316,7 +317,7 @@ function AppHome({app}: Props): JSX.Element
|
|||||||
<Grid key={table.name} item xs={12} md={12} lg={tileSizeLg}>
|
<Grid key={table.name} item xs={12} md={12} lg={tileSizeLg}>
|
||||||
{hasTablePermission(tableName) ?
|
{hasTablePermission(tableName) ?
|
||||||
<Link to={table.name}>
|
<Link to={table.name}>
|
||||||
<Box mb={3}>
|
<Box className="big-icon" mb={3}>
|
||||||
<MiniStatisticsCard
|
<MiniStatisticsCard
|
||||||
title={{fontWeight: "bold", text: table.label}}
|
title={{fontWeight: "bold", text: table.label}}
|
||||||
count={!tableCounts.has(table.name) || tableCounts.get(table.name).isLoading ? "..." : (tableCountNumbers.get(table.name))}
|
count={!tableCounts.has(table.name) || tableCounts.get(table.name).isLoading ? "..." : (tableCountNumbers.get(table.name))}
|
||||||
|
@ -27,7 +27,6 @@ import {QJobComplete} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJo
|
|||||||
import {QJobError} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobError";
|
import {QJobError} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobError";
|
||||||
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
||||||
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
|
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
|
||||||
import {TablePagination} from "@mui/material";
|
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
import Grid from "@mui/material/Grid";
|
import Grid from "@mui/material/Grid";
|
||||||
@ -38,6 +37,7 @@ import {DataGridPro, GridSortModel} from "@mui/x-data-grid-pro";
|
|||||||
import FormData from "form-data";
|
import FormData from "form-data";
|
||||||
import React, {useEffect, useState} from "react";
|
import React, {useEffect, useState} from "react";
|
||||||
import DataGridUtils from "qqq/utils/DataGridUtils";
|
import DataGridUtils from "qqq/utils/DataGridUtils";
|
||||||
|
import HtmlUtils from "qqq/utils/HtmlUtils";
|
||||||
import Client from "qqq/utils/qqq/Client";
|
import Client from "qqq/utils/qqq/Client";
|
||||||
import ValueUtils from "qqq/utils/qqq/ValueUtils";
|
import ValueUtils from "qqq/utils/qqq/ValueUtils";
|
||||||
|
|
||||||
@ -97,6 +97,8 @@ function ColumnStats({tableMetaData, fieldMetaData, fieldTableName, filter}: Pro
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
// todo - job running!
|
||||||
|
|
||||||
const result = processResult as QJobComplete;
|
const result = processResult as QJobComplete;
|
||||||
|
|
||||||
const statFieldObjects = result.values.statsFields;
|
const statFieldObjects = result.values.statsFields;
|
||||||
@ -174,6 +176,20 @@ function ColumnStats({tableMetaData, fieldMetaData, fieldTableName, filter}: Pro
|
|||||||
setStatusString("Refreshing...")
|
setStatusString("Refreshing...")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const doExport = () =>
|
||||||
|
{
|
||||||
|
let csv = `"${ValueUtils.cleanForCsv(fieldMetaData.label)}","Count"\n`;
|
||||||
|
for (let i = 0; i < valueCounts.length; i++)
|
||||||
|
{
|
||||||
|
const fieldValue = valueCounts[i].displayValues.get(fieldMetaData.name);
|
||||||
|
const count = valueCounts[i].values.get("count");
|
||||||
|
csv += `"${ValueUtils.cleanForCsv(fieldValue)}",${count}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileName = tableMetaData.label + " - " + fieldMetaData.label + " Column Stats " + ValueUtils.formatDateTimeForFileName(new Date()) + ".csv";
|
||||||
|
HtmlUtils.download(fileName, csv);
|
||||||
|
}
|
||||||
|
|
||||||
function Loading()
|
function Loading()
|
||||||
{
|
{
|
||||||
return (
|
return (
|
||||||
@ -200,9 +216,14 @@ function ColumnStats({tableMetaData, fieldMetaData, fieldTableName, filter}: Pro
|
|||||||
{statusString ?? <> </>}
|
{statusString ?? <> </>}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Typography>
|
</Typography>
|
||||||
|
<Box>
|
||||||
<Button onClick={() => refresh()} startIcon={<Icon>refresh</Icon>}>
|
<Button onClick={() => refresh()} startIcon={<Icon>refresh</Icon>}>
|
||||||
Refresh
|
Refresh
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button onClick={() => doExport()} startIcon={<Icon>save_alt</Icon>} disabled={valueCounts == null || valueCounts.length == 0}>
|
||||||
|
Export
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<Grid container>
|
<Grid container>
|
||||||
<Grid item xs={8}>
|
<Grid item xs={8}>
|
||||||
|
@ -386,16 +386,15 @@ input[type="search"]::-webkit-search-results-decoration { display: none; }
|
|||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.MuiGrid-root > .MuiBox-root > .material-icons-round,
|
.big-icon .material-icons-round
|
||||||
.MuiBox-root > .MuiBox-root > .material-icons-round
|
|
||||||
{
|
{
|
||||||
font-size: 2rem !important;
|
font-size: 2rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-schedule-icon
|
.dashboard-schedule-icon
|
||||||
{
|
{
|
||||||
font-size: 1rem !important;
|
font-size: 1.1rem !important;
|
||||||
position: relative;
|
position: relative;
|
||||||
top: -13px;
|
top: -5px;
|
||||||
margin-right: 3px;
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,9 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Utility functions for basic html/webpage/browser things.
|
||||||
|
*******************************************************************************/
|
||||||
export default class HtmlUtils
|
export default class HtmlUtils
|
||||||
{
|
{
|
||||||
|
|
||||||
@ -39,4 +42,21 @@ export default class HtmlUtils
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Download a client-side generated file (e.g., csv).
|
||||||
|
*******************************************************************************/
|
||||||
|
static download = (filename: string, text: string) =>
|
||||||
|
{
|
||||||
|
var element = document.createElement("a");
|
||||||
|
element.setAttribute("href", "data:text/plain;charset=utf-8," + encodeURIComponent(text));
|
||||||
|
element.setAttribute("download", filename);
|
||||||
|
|
||||||
|
element.style.display = "none";
|
||||||
|
document.body.appendChild(element);
|
||||||
|
|
||||||
|
element.click();
|
||||||
|
|
||||||
|
document.body.removeChild(element);
|
||||||
|
};
|
||||||
|
|
||||||
}
|
}
|
@ -65,7 +65,7 @@ class FilterUtils
|
|||||||
return QCriteriaOperator.EQUALS;
|
return QCriteriaOperator.EQUALS;
|
||||||
case "isNot":
|
case "isNot":
|
||||||
case "!=":
|
case "!=":
|
||||||
return QCriteriaOperator.NOT_EQUALS;
|
return QCriteriaOperator.NOT_EQUALS_OR_IS_NULL;
|
||||||
case "after":
|
case "after":
|
||||||
case ">":
|
case ">":
|
||||||
return QCriteriaOperator.GREATER_THAN;
|
return QCriteriaOperator.GREATER_THAN;
|
||||||
@ -138,6 +138,7 @@ class FilterUtils
|
|||||||
return ("is");
|
return ("is");
|
||||||
}
|
}
|
||||||
case QCriteriaOperator.NOT_EQUALS:
|
case QCriteriaOperator.NOT_EQUALS:
|
||||||
|
case QCriteriaOperator.NOT_EQUALS_OR_IS_NULL:
|
||||||
|
|
||||||
if (field.possibleValueSourceName)
|
if (field.possibleValueSourceName)
|
||||||
{
|
{
|
||||||
|
@ -273,6 +273,14 @@ class ValueUtils
|
|||||||
return (`${date.toString("yyyy-MM-ddTHH:mm:ssZ")}`);
|
return (`${date.toString("yyyy-MM-ddTHH:mm:ssZ")}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static formatDateTimeForFileName(date: Date)
|
||||||
|
{
|
||||||
|
const zp = (value: number): string => (value < 10 ? `0${value}` : `${value}`);
|
||||||
|
const d = new Date();
|
||||||
|
const dateString = `${d.getFullYear()}-${zp(d.getMonth() + 1)}-${zp(d.getDate())} ${zp(d.getHours())}${zp(d.getMinutes())}`;
|
||||||
|
return (dateString);
|
||||||
|
}
|
||||||
|
|
||||||
public static getFullWeekday(date: Date)
|
public static getFullWeekday(date: Date)
|
||||||
{
|
{
|
||||||
if (!(date instanceof Date))
|
if (!(date instanceof Date))
|
||||||
@ -410,6 +418,19 @@ class ValueUtils
|
|||||||
return toPush;
|
return toPush;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** for building CSV in frontends, cleanse null & undefined, and escape "'s
|
||||||
|
*******************************************************************************/
|
||||||
|
public static cleanForCsv(param: any): string
|
||||||
|
{
|
||||||
|
if(param === undefined || param === null)
|
||||||
|
{
|
||||||
|
return ("");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (String(param).replaceAll(/"/g, "\"\""));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
Reference in New Issue
Block a user