mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-22 07:08:44 +00:00
Compare commits
1 Commits
snapshot-f
...
snapshot-f
Author | SHA1 | Date | |
---|---|---|---|
575ffe761f |
20
src/App.tsx
20
src/App.tsx
@ -73,14 +73,6 @@ export default function App()
|
|||||||
const [loggedInUser, setLoggedInUser] = useState({} as { name?: string, email?: string });
|
const [loggedInUser, setLoggedInUser] = useState({} as { name?: string, email?: string });
|
||||||
const [defaultRoute, setDefaultRoute] = useState("/no-apps");
|
const [defaultRoute, setDefaultRoute] = useState("/no-apps");
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////
|
|
||||||
// tell the client how to do a logout if it sees a 401 //
|
|
||||||
/////////////////////////////////////////////////////////
|
|
||||||
Client.setUnauthorizedCallback(() =>
|
|
||||||
{
|
|
||||||
logout();
|
|
||||||
})
|
|
||||||
|
|
||||||
const shouldStoreNewToken = (newToken: string, oldToken: string): boolean =>
|
const shouldStoreNewToken = (newToken: string, oldToken: string): boolean =>
|
||||||
{
|
{
|
||||||
if (!cookies[SESSION_UUID_COOKIE_NAME])
|
if (!cookies[SESSION_UUID_COOKIE_NAME])
|
||||||
@ -175,8 +167,18 @@ export default function App()
|
|||||||
console.log("Using existing sessionUUID cookie");
|
console.log("Using existing sessionUUID cookie");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// todo#authHeader - this is our quick rollback plan - if we feel the need to stop using the cookie approach. //
|
||||||
|
// we turn off the shouldStoreNewToken block above, and turn on these 2 lines. //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
removeCookie(SESSION_UUID_COOKIE_NAME, {path: "/"});
|
||||||
|
localStorage.removeItem("accessToken");
|
||||||
|
*/
|
||||||
|
|
||||||
setIsFullyAuthenticated(true);
|
setIsFullyAuthenticated(true);
|
||||||
qController.setGotAuthentication();
|
qController.setGotAuthentication();
|
||||||
|
qController.setAuthorizationHeaderValue("Bearer " + accessToken);
|
||||||
|
|
||||||
setLoggedInUser(user);
|
setLoggedInUser(user);
|
||||||
console.log("Token load complete.");
|
console.log("Token load complete.");
|
||||||
@ -197,8 +199,8 @@ export default function App()
|
|||||||
// use a random token if anonymous or mock //
|
// use a random token if anonymous or mock //
|
||||||
/////////////////////////////////////////////
|
/////////////////////////////////////////////
|
||||||
console.log("Generating random token...");
|
console.log("Generating random token...");
|
||||||
|
qController.setAuthorizationHeaderValue(Md5.hashStr(`${new Date()}`));
|
||||||
setIsFullyAuthenticated(true);
|
setIsFullyAuthenticated(true);
|
||||||
qController.setGotAuthentication();
|
|
||||||
setCookie(SESSION_UUID_COOKIE_NAME, Md5.hashStr(`${new Date()}`), {path: "/"});
|
setCookie(SESSION_UUID_COOKIE_NAME, Md5.hashStr(`${new Date()}`), {path: "/"});
|
||||||
console.log("Token generation complete.");
|
console.log("Token generation complete.");
|
||||||
return;
|
return;
|
||||||
|
@ -34,6 +34,7 @@ import DialogContent from "@mui/material/DialogContent";
|
|||||||
import DialogTitle from "@mui/material/DialogTitle";
|
import DialogTitle from "@mui/material/DialogTitle";
|
||||||
import Grid from "@mui/material/Grid";
|
import Grid from "@mui/material/Grid";
|
||||||
import Icon from "@mui/material/Icon";
|
import Icon from "@mui/material/Icon";
|
||||||
|
import IconButton from "@mui/material/IconButton";
|
||||||
import TextField from "@mui/material/TextField";
|
import TextField from "@mui/material/TextField";
|
||||||
import React, {useState} from "react";
|
import React, {useState} from "react";
|
||||||
import {useNavigate} from "react-router-dom";
|
import {useNavigate} from "react-router-dom";
|
||||||
@ -195,8 +196,17 @@ function GotoRecordDialog(props: Props): JSX.Element
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={props.isOpen} onClose={() => closeRequested} onKeyPress={(e) => keyPressed(e)} fullWidth maxWidth={"sm"}>
|
<Dialog open={props.isOpen} onClose={() => closeRequested} onKeyPress={(e) => keyPressed(e)} fullWidth maxWidth={"sm"}>
|
||||||
<DialogTitle>Go To...</DialogTitle>
|
<DialogTitle sx={{display: "flex"}}>
|
||||||
|
<Box sx={{display: "flex", flexGrow: 1}}>
|
||||||
|
Go To...
|
||||||
|
</Box>
|
||||||
|
<Box sx={{display: "flex"}}>
|
||||||
|
<IconButton onClick={() =>
|
||||||
|
{
|
||||||
|
document.location.href = "/";
|
||||||
|
}}><Icon sx={{align: "right"}} fontSize="small">close</Icon></IconButton>
|
||||||
|
</Box>
|
||||||
|
</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
{props.subHeader}
|
{props.subHeader}
|
||||||
{
|
{
|
||||||
|
@ -30,7 +30,7 @@ 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, {useEffect, useState} from "react";
|
import React, {useEffect, useState} from "react";
|
||||||
import {NavigateFunction, useNavigate} from "react-router-dom";
|
import {Link, NavigateFunction, useNavigate} from "react-router-dom";
|
||||||
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";
|
||||||
|
|
||||||
@ -46,7 +46,6 @@ export interface WidgetData
|
|||||||
dropdownNeedsSelectedText?: string;
|
dropdownNeedsSelectedText?: string;
|
||||||
hasPermission?: boolean;
|
hasPermission?: boolean;
|
||||||
errorLoading?: boolean;
|
errorLoading?: boolean;
|
||||||
|
|
||||||
[other: string]: any;
|
[other: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,7 +53,6 @@ export interface WidgetData
|
|||||||
interface Props
|
interface Props
|
||||||
{
|
{
|
||||||
labelAdditionalComponentsLeft: LabelComponent[];
|
labelAdditionalComponentsLeft: LabelComponent[];
|
||||||
labelAdditionalElementsLeft: JSX.Element[];
|
|
||||||
labelAdditionalComponentsRight: LabelComponent[];
|
labelAdditionalComponentsRight: LabelComponent[];
|
||||||
widgetMetaData?: QWidgetMetaData;
|
widgetMetaData?: QWidgetMetaData;
|
||||||
widgetData?: WidgetData;
|
widgetData?: WidgetData;
|
||||||
@ -72,7 +70,6 @@ Widget.defaultProps = {
|
|||||||
widgetMetaData: {},
|
widgetMetaData: {},
|
||||||
widgetData: {},
|
widgetData: {},
|
||||||
labelAdditionalComponentsLeft: [],
|
labelAdditionalComponentsLeft: [],
|
||||||
labelAdditionalElementsLeft: [],
|
|
||||||
labelAdditionalComponentsRight: [],
|
labelAdditionalComponentsRight: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -91,8 +88,34 @@ export class LabelComponent
|
|||||||
{
|
{
|
||||||
render = (args: LabelComponentRenderArgs): JSX.Element =>
|
render = (args: LabelComponentRenderArgs): JSX.Element =>
|
||||||
{
|
{
|
||||||
return (<div>Unsupported component type</div>);
|
return (<div>Unsupported component type</div>)
|
||||||
};
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
export class HeaderLink extends LabelComponent
|
||||||
|
{
|
||||||
|
label: string;
|
||||||
|
to: string
|
||||||
|
|
||||||
|
constructor(label: string, to: string)
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
this.label = label;
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -118,8 +141,8 @@ export class AddNewRecordButton extends LabelComponent
|
|||||||
|
|
||||||
openEditForm = (navigate: any, table: QTableMetaData, id: any = null, defaultValues: any, disabledFields: any) =>
|
openEditForm = (navigate: any, table: QTableMetaData, id: any = null, defaultValues: any, disabledFields: any) =>
|
||||||
{
|
{
|
||||||
navigate(`#/createChild=${table.name}/defaultValues=${JSON.stringify(defaultValues)}/disabledFields=${JSON.stringify(disabledFields)}`);
|
navigate(`#/createChild=${table.name}/defaultValues=${JSON.stringify(defaultValues)}/disabledFields=${JSON.stringify(disabledFields)}`)
|
||||||
};
|
}
|
||||||
|
|
||||||
render = (args: LabelComponentRenderArgs): JSX.Element =>
|
render = (args: LabelComponentRenderArgs): JSX.Element =>
|
||||||
{
|
{
|
||||||
@ -128,7 +151,35 @@ export class AddNewRecordButton extends LabelComponent
|
|||||||
<Button sx={{mt: 0.75}} onClick={() => this.openEditForm(args.navigate, this.table, null, this.defaultValues, this.disabledFields)}>{this.label}</Button>
|
<Button sx={{mt: 0.75}} onClick={() => this.openEditForm(args.navigate, this.table, null, this.defaultValues, this.disabledFields)}>{this.label}</Button>
|
||||||
</Typography>
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -176,7 +227,7 @@ export class Dropdown extends LabelComponent
|
|||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -200,7 +251,7 @@ export class ReloadControl extends LabelComponent
|
|||||||
<Tooltip title="Refresh"><Button sx={{px: 1, py: 0, minWidth: "initial"}} onClick={() => this.callback()}><Icon>refresh</Icon></Button></Tooltip>
|
<Tooltip title="Refresh"><Button sx={{px: 1, py: 0, minWidth: "initial"}} onClick={() => this.callback()}><Icon>refresh</Icon></Button></Tooltip>
|
||||||
</Typography>
|
</Typography>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -343,7 +394,7 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
reloadWidget(dropdownData);
|
reloadWidget(dropdownData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -371,7 +422,7 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
|||||||
{
|
{
|
||||||
console.log(`No reload widget callback in ${props.widgetMetaData.label}`);
|
console.log(`No reload widget callback in ${props.widgetMetaData.label}`);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const toggleFullScreenWidget = () =>
|
const toggleFullScreenWidget = () =>
|
||||||
{
|
{
|
||||||
@ -383,14 +434,14 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
|||||||
{
|
{
|
||||||
setFullScreenWidgetClassName("fullScreenWidget");
|
setFullScreenWidgetClassName("fullScreenWidget");
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
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 =>
|
const isSet = (v: any): boolean =>
|
||||||
{
|
{
|
||||||
return (v !== null && v !== undefined);
|
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 //
|
// 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 //
|
||||||
@ -399,7 +450,6 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
|||||||
if (hasPermission)
|
if (hasPermission)
|
||||||
{
|
{
|
||||||
needLabelBox ||= (labelComponentsLeft && labelComponentsLeft.length > 0);
|
needLabelBox ||= (labelComponentsLeft && labelComponentsLeft.length > 0);
|
||||||
needLabelBox ||= (props.labelAdditionalElementsLeft && props.labelAdditionalElementsLeft.length > 0);
|
|
||||||
needLabelBox ||= (labelComponentsRight && labelComponentsRight.length > 0);
|
needLabelBox ||= (labelComponentsRight && labelComponentsRight.length > 0);
|
||||||
needLabelBox ||= isSet(props.widgetMetaData?.icon);
|
needLabelBox ||= isSet(props.widgetMetaData?.icon);
|
||||||
needLabelBox ||= isSet(props.widgetData?.label);
|
needLabelBox ||= isSet(props.widgetData?.label);
|
||||||
@ -480,7 +530,6 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
{props.labelAdditionalElementsLeft}
|
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
{
|
{
|
||||||
|
@ -22,14 +22,10 @@
|
|||||||
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 {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
||||||
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 {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, Link} from "react-router-dom";
|
import {useNavigate} from "react-router-dom";
|
||||||
import Widget, {AddNewRecordButton, 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 HtmlUtils from "qqq/utils/HtmlUtils";
|
||||||
import Client from "qqq/utils/qqq/Client";
|
import Client from "qqq/utils/qqq/Client";
|
||||||
@ -51,8 +47,6 @@ function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element
|
|||||||
const [records, setRecords] = useState([] as QRecord[])
|
const [records, setRecords] = useState([] as QRecord[])
|
||||||
const [columns, setColumns] = useState([]);
|
const [columns, setColumns] = useState([]);
|
||||||
const [allColumns, setAllColumns] = useState([])
|
const [allColumns, setAllColumns] = useState([])
|
||||||
const [csv, setCsv] = useState(null as string);
|
|
||||||
const [fileName, setFileName] = useState(null as string);
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
@ -81,7 +75,6 @@ function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element
|
|||||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// capture all-columns to use for the export (before we might splice some away from the on-screen display) //
|
// capture all-columns to use for the export (before we might splice some away from the on-screen display) //
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
const allColumns = [... columns];
|
|
||||||
setAllColumns(JSON.parse(JSON.stringify(columns)));
|
setAllColumns(JSON.parse(JSON.stringify(columns)));
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////
|
||||||
@ -102,7 +95,11 @@ function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element
|
|||||||
setRows(rows);
|
setRows(rows);
|
||||||
setRecords(records)
|
setRecords(records)
|
||||||
setColumns(columns);
|
setColumns(columns);
|
||||||
|
}
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
const exportCallback = () =>
|
||||||
|
{
|
||||||
let csv = "";
|
let csv = "";
|
||||||
for (let i = 0; i < allColumns.length; i++)
|
for (let i = 0; i < allColumns.length; i++)
|
||||||
{
|
{
|
||||||
@ -121,23 +118,16 @@ function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fileName = (data?.label ?? widgetMetaData.label) + " " + ValueUtils.formatDateTimeForFileName(new Date()) + ".csv";
|
const fileName = (data?.label ?? widgetMetaData.label) + " " + ValueUtils.formatDateTimeForFileName(new Date()) + ".csv";
|
||||||
|
HtmlUtils.download(fileName, csv);
|
||||||
setCsv(csv);
|
|
||||||
setFileName(fileName);
|
|
||||||
}
|
}
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
///////////////////
|
///////////////////
|
||||||
// view all link //
|
// view all link //
|
||||||
///////////////////
|
///////////////////
|
||||||
const labelAdditionalElementsLeft: JSX.Element[] = [];
|
const labelAdditionalComponentsLeft: LabelComponent[] = []
|
||||||
if(data && data.viewAllLink)
|
if(data && data.viewAllLink)
|
||||||
{
|
{
|
||||||
labelAdditionalElementsLeft.push(
|
labelAdditionalComponentsLeft.push(new HeaderLink("View All", data.viewAllLink));
|
||||||
<Typography variant="body2" p={2} display="inline" fontSize=".875rem" pt="0" position="relative" top="-0.25rem">
|
|
||||||
<Link to={data.viewAllLink}>View All</Link>
|
|
||||||
</Typography>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////
|
///////////////////
|
||||||
@ -159,26 +149,7 @@ function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onExportClick = () =>
|
labelAdditionalComponentsLeft.push(new ExportDataButton(() => exportCallback(), isExportDisabled, tooltipTitle))
|
||||||
{
|
|
||||||
if(csv)
|
|
||||||
{
|
|
||||||
HtmlUtils.download(fileName, csv);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
alert("There is no data available to export.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(widgetMetaData?.showExportButton)
|
|
||||||
{
|
|
||||||
labelAdditionalElementsLeft.push(
|
|
||||||
<Typography key={1} variant="body2" py={2} px={0} display="inline" position="relative" top="-0.375rem">
|
|
||||||
<Tooltip title={tooltipTitle}><Button sx={{px: 1, py: 0, minWidth: "initial"}} onClick={onExportClick} disabled={isExportDisabled}><Icon>save_alt</Icon></Button></Tooltip>
|
|
||||||
</Typography>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////
|
////////////////////
|
||||||
// add new button //
|
// add new button //
|
||||||
@ -213,7 +184,7 @@ function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element
|
|||||||
<Widget
|
<Widget
|
||||||
widgetMetaData={widgetMetaData}
|
widgetMetaData={widgetMetaData}
|
||||||
widgetData={data}
|
widgetData={data}
|
||||||
labelAdditionalElementsLeft={labelAdditionalElementsLeft}
|
labelAdditionalComponentsLeft={labelAdditionalComponentsLeft}
|
||||||
labelAdditionalComponentsRight={labelAdditionalComponentsRight}
|
labelAdditionalComponentsRight={labelAdditionalComponentsRight}
|
||||||
>
|
>
|
||||||
<DataGridPro
|
<DataGridPro
|
||||||
|
@ -21,15 +21,11 @@
|
|||||||
|
|
||||||
|
|
||||||
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
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";
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import {htmlToText} from "html-to-text";
|
import {htmlToText} from "html-to-text";
|
||||||
import React, {useEffect, useState} from "react";
|
import React, {useEffect, useState} from "react";
|
||||||
import TableCard from "qqq/components/widgets/tables/TableCard";
|
import TableCard from "qqq/components/widgets/tables/TableCard";
|
||||||
import Widget, {WidgetData} from "qqq/components/widgets/Widget";
|
import Widget, {ExportDataButton, WidgetData} from "qqq/components/widgets/Widget";
|
||||||
import HtmlUtils from "qqq/utils/HtmlUtils";
|
import HtmlUtils from "qqq/utils/HtmlUtils";
|
||||||
import ValueUtils from "qqq/utils/qqq/ValueUtils";
|
import ValueUtils from "qqq/utils/qqq/ValueUtils";
|
||||||
|
|
||||||
@ -47,8 +43,6 @@ TableWidget.defaultProps = {
|
|||||||
function TableWidget(props: Props): JSX.Element
|
function TableWidget(props: Props): JSX.Element
|
||||||
{
|
{
|
||||||
const [isExportDisabled, setIsExportDisabled] = useState(false); // hmm, would like true here, but it broke...
|
const [isExportDisabled, setIsExportDisabled] = useState(false); // hmm, would like true here, but it broke...
|
||||||
const [csv, setCsv] = useState(null as string);
|
|
||||||
const [fileName, setFileName] = useState(null as string);
|
|
||||||
|
|
||||||
const rows = props.widgetData?.rows;
|
const rows = props.widgetData?.rows;
|
||||||
const columns = props.widgetData?.columns;
|
const columns = props.widgetData?.columns;
|
||||||
@ -62,8 +56,14 @@ function TableWidget(props: Props): JSX.Element
|
|||||||
}
|
}
|
||||||
setIsExportDisabled(isExportDisabled);
|
setIsExportDisabled(isExportDisabled);
|
||||||
|
|
||||||
|
}, [props.widgetMetaData, props.widgetData]);
|
||||||
|
|
||||||
|
const exportCallback = () =>
|
||||||
|
{
|
||||||
if (props.widgetData && rows && columns)
|
if (props.widgetData && rows && columns)
|
||||||
{
|
{
|
||||||
|
console.log(props.widgetData);
|
||||||
|
|
||||||
let csv = "";
|
let csv = "";
|
||||||
for (let j = 0; j < columns.length; j++)
|
for (let j = 0; j < columns.length; j++)
|
||||||
{
|
{
|
||||||
@ -98,37 +98,16 @@ function TableWidget(props: Props): JSX.Element
|
|||||||
csv += "\n";
|
csv += "\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
setCsv(csv);
|
console.log(csv);
|
||||||
|
|
||||||
const fileName = (props.widgetData.label ?? props.widgetMetaData.label) + " " + ValueUtils.formatDateTimeForFileName(new Date()) + ".csv";
|
const fileName = (props.widgetData.label ?? props.widgetMetaData.label) + " " + ValueUtils.formatDateTimeForFileName(new Date()) + ".csv";
|
||||||
setFileName(fileName)
|
|
||||||
|
|
||||||
console.log(`useEffect, setting fileName ${fileName}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
}, [props.widgetMetaData, props.widgetData]);
|
|
||||||
|
|
||||||
const onExportClick = () =>
|
|
||||||
{
|
|
||||||
if(csv)
|
|
||||||
{
|
|
||||||
HtmlUtils.download(fileName, csv);
|
HtmlUtils.download(fileName, csv);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
alert("There is no data available to export.")
|
alert("There is no data available to export.");
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const labelAdditionalElementsLeft: JSX.Element[] = [];
|
|
||||||
if(props.widgetMetaData?.showExportButton)
|
|
||||||
{
|
|
||||||
labelAdditionalElementsLeft.push(
|
|
||||||
<Typography key={1} variant="body2" py={2} px={0} display="inline" position="relative" top="-0.375rem">
|
|
||||||
<Tooltip title="Export"><Button sx={{px: 1, py: 0, minWidth: "initial"}} onClick={onExportClick} disabled={false}><Icon>save_alt</Icon></Button></Tooltip>
|
|
||||||
</Typography>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Widget
|
<Widget
|
||||||
@ -137,7 +116,7 @@ function TableWidget(props: Props): JSX.Element
|
|||||||
reloadWidgetCallback={(data) => props.reloadWidgetCallback(data)}
|
reloadWidgetCallback={(data) => props.reloadWidgetCallback(data)}
|
||||||
footerHTML={props.widgetData?.footerHTML}
|
footerHTML={props.widgetData?.footerHTML}
|
||||||
isChild={props.isChild}
|
isChild={props.isChild}
|
||||||
labelAdditionalElementsLeft={labelAdditionalElementsLeft}
|
labelAdditionalComponentsLeft={props.widgetMetaData?.showExportButton ? [new ExportDataButton(() => exportCallback(), isExportDisabled)] : []}
|
||||||
>
|
>
|
||||||
<TableCard
|
<TableCard
|
||||||
noRowsFoundHTML={props.widgetData?.noRowsFoundHTML}
|
noRowsFoundHTML={props.widgetData?.noRowsFoundHTML}
|
||||||
|
@ -229,7 +229,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
const download = (url: string, fileName: string) =>
|
const download = (url: string, fileName: string) =>
|
||||||
{
|
{
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// todo - this could be simplified, i think? //
|
// todo - this could be simplified. //
|
||||||
// it was originally built like this when we had to submit full access token to backend... //
|
// it was originally built like this when we had to submit full access token to backend... //
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
let xhr = new XMLHttpRequest();
|
let xhr = new XMLHttpRequest();
|
||||||
@ -237,6 +237,12 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
xhr.responseType = "blob";
|
xhr.responseType = "blob";
|
||||||
let formData = new FormData();
|
let formData = new FormData();
|
||||||
|
|
||||||
|
////////////////////////////////////
|
||||||
|
// todo#authHeader - delete this. //
|
||||||
|
////////////////////////////////////
|
||||||
|
const qController = Client.getInstance();
|
||||||
|
formData.append("Authorization", qController.getAuthorizationHeaderValue());
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
xhr.send(formData);
|
xhr.send(formData);
|
||||||
|
|
||||||
|
@ -55,7 +55,7 @@ import {DataGridPro, GridCallbackDetails, GridColDef, GridColumnMenuContainer, G
|
|||||||
import {GridRowModel} from "@mui/x-data-grid/models/gridRows";
|
import {GridRowModel} from "@mui/x-data-grid/models/gridRows";
|
||||||
import FormData from "form-data";
|
import FormData from "form-data";
|
||||||
import React, {forwardRef, useContext, useEffect, useReducer, useRef, useState} from "react";
|
import React, {forwardRef, useContext, useEffect, useReducer, useRef, useState} from "react";
|
||||||
import {useLocation, useNavigate, useSearchParams} from "react-router-dom";
|
import {Navigate, NavigateFunction, useLocation, useNavigate, useSearchParams} from "react-router-dom";
|
||||||
import QContext from "QContext";
|
import QContext from "QContext";
|
||||||
import {QActionsMenuButton, QCancelButton, QCreateNewButton, QSaveButton} from "qqq/components/buttons/DefaultButtons";
|
import {QActionsMenuButton, QCancelButton, QCreateNewButton, QSaveButton} from "qqq/components/buttons/DefaultButtons";
|
||||||
import MenuButton from "qqq/components/buttons/MenuButton";
|
import MenuButton from "qqq/components/buttons/MenuButton";
|
||||||
@ -752,7 +752,42 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
console.log(`Received error for query ${thisQueryId}`);
|
console.log(`Received error for query ${thisQueryId}`);
|
||||||
console.log(error);
|
console.log(error);
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// special case for variant errors, if 500 and certain message, just clear out //
|
||||||
|
// local storage of variant and reload the page (rather than black page of death) //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////
|
||||||
var errorMessage;
|
var errorMessage;
|
||||||
|
if(tableMetaData?.usesVariants)
|
||||||
|
{
|
||||||
|
if (error.status == "500" && error.message.indexOf("Could not find Backend Variant") != -1)
|
||||||
|
{
|
||||||
|
if (table)
|
||||||
|
{
|
||||||
|
const tableVariantLocalStorageKey = `${TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT}.${table.name}`;
|
||||||
|
localStorage.removeItem(tableVariantLocalStorageKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (error && error.message)
|
||||||
|
{
|
||||||
|
errorMessage = error.message;
|
||||||
|
}
|
||||||
|
else if (error && error.response && error.response.data && error.response.data.error)
|
||||||
|
{
|
||||||
|
errorMessage = error.response.data.error;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
errorMessage = "Unexpected error running query";
|
||||||
|
}
|
||||||
|
|
||||||
|
setAlertContent(errorMessage);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
if (error && error.message)
|
if (error && error.message)
|
||||||
{
|
{
|
||||||
errorMessage = error.message;
|
errorMessage = error.message;
|
||||||
@ -771,7 +806,8 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
setReceivedQueryErrorTimestamp(new Date());
|
setReceivedQueryErrorTimestamp(new Date());
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
});
|
}
|
||||||
|
})
|
||||||
})();
|
})();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1136,6 +1172,8 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
<body>
|
<body>
|
||||||
Generating file <u>${filename}</u>${totalRecords ? " with " + totalRecords.toLocaleString() + " record" + (totalRecords == 1 ? "" : "s") : ""}...
|
Generating file <u>${filename}</u>${totalRecords ? " with " + totalRecords.toLocaleString() + " record" + (totalRecords == 1 ? "" : "s") : ""}...
|
||||||
<form id="exportForm" method="post" action="${url}" >
|
<form id="exportForm" method="post" action="${url}" >
|
||||||
|
<!-- todo#authHeader - remove this. -->
|
||||||
|
<input type="hidden" name="Authorization" value="${qController.getAuthorizationHeaderValue()}">
|
||||||
<input type="hidden" name="fields" value="${visibleFields.join(",")}">
|
<input type="hidden" name="fields" value="${visibleFields.join(",")}">
|
||||||
<input type="hidden" name="filter" id="filter">
|
<input type="hidden" name="filter" id="filter">
|
||||||
</form>
|
</form>
|
||||||
@ -1887,7 +1925,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
{
|
{
|
||||||
return (
|
return (
|
||||||
<BaseLayout>
|
<BaseLayout>
|
||||||
<TableVariantDialog table={tableMetaData} isOpen={true} closeHandler={(value: QTableVariant) =>
|
<TableVariantDialog navigate={navigate} table={tableMetaData} isOpen={true} closeHandler={(value: QTableVariant) =>
|
||||||
{
|
{
|
||||||
setTableVariantPromptOpen(false);
|
setTableVariantPromptOpen(false);
|
||||||
setTableVariant(value);
|
setTableVariant(value);
|
||||||
@ -2057,7 +2095,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
|
|
||||||
{
|
{
|
||||||
tableMetaData &&
|
tableMetaData &&
|
||||||
<TableVariantDialog table={tableMetaData} isOpen={tableVariantPromptOpen} closeHandler={(value: QTableVariant) =>
|
<TableVariantDialog navigate={navigate} table={tableMetaData} isOpen={tableVariantPromptOpen} closeHandler={(value: QTableVariant) =>
|
||||||
{
|
{
|
||||||
setTableVariantPromptOpen(false);
|
setTableVariantPromptOpen(false);
|
||||||
setTableVariant(value);
|
setTableVariant(value);
|
||||||
@ -2089,7 +2127,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// mini-component that is the dialog for the user to select a variant on tables with variant backends //
|
// mini-component that is the dialog for the user to select a variant on tables with variant backends //
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
function TableVariantDialog(props: {isOpen: boolean; table: QTableMetaData; closeHandler: (value?: QTableVariant) => void})
|
function TableVariantDialog(props: {navigate: NavigateFunction, isOpen: boolean; table: QTableMetaData; closeHandler: (value?: QTableVariant) => void})
|
||||||
{
|
{
|
||||||
const [value, setValue] = useState(null)
|
const [value, setValue] = useState(null)
|
||||||
const [dropDownOpen, setDropDownOpen] = useState(false)
|
const [dropDownOpen, setDropDownOpen] = useState(false)
|
||||||
@ -2138,7 +2176,17 @@ function TableVariantDialog(props: {isOpen: boolean; table: QTableMetaData; clos
|
|||||||
|
|
||||||
return variants && (
|
return variants && (
|
||||||
<Dialog open={props.isOpen} onKeyPress={(e) => keyPressed(e)}>
|
<Dialog open={props.isOpen} onKeyPress={(e) => keyPressed(e)}>
|
||||||
<DialogTitle>{props.table.variantTableLabel}</DialogTitle>
|
<DialogTitle sx={{display: "flex"}}>
|
||||||
|
<Box sx={{display: "flex", flexGrow: 1}}>
|
||||||
|
{props.table.variantTableLabel}
|
||||||
|
</Box>
|
||||||
|
<Box sx={{display: "flex"}}>
|
||||||
|
<IconButton onClick={() =>
|
||||||
|
{
|
||||||
|
document.location.href = "/";
|
||||||
|
}}><Icon sx={{align: "right"}} fontSize="small">close</Icon></IconButton>
|
||||||
|
</Box>
|
||||||
|
</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogContentText>Select the {props.table.variantTableLabel} to be used on this table:</DialogContentText>
|
<DialogContentText>Select the {props.table.variantTableLabel} to be used on this table:</DialogContentText>
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
|
@ -441,6 +441,8 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(record)
|
||||||
|
{
|
||||||
setPageHeader(record.recordLabel);
|
setPageHeader(record.recordLabel);
|
||||||
|
|
||||||
if(!launchingProcess)
|
if(!launchingProcess)
|
||||||
@ -454,6 +456,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
|||||||
console.error("Error pushing history: " + e);
|
console.error("Error pushing history: " + e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/////////////////////////////////////////////////
|
/////////////////////////////////////////////////
|
||||||
// define the sections, e.g., for the left-bar //
|
// define the sections, e.g., for the left-bar //
|
||||||
|
@ -63,10 +63,6 @@ export default class HtmlUtils
|
|||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Download a server-side generated file (or the contents of a data: url)
|
** Download a server-side generated file (or the contents of a data: url)
|
||||||
**
|
|
||||||
** todo - this could be simplified (i think?)
|
|
||||||
** it was originally built like this when we had to submit full access token to backend...
|
|
||||||
**
|
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
static downloadUrlViaIFrame = (url: string, filename: string) =>
|
static downloadUrlViaIFrame = (url: string, filename: string) =>
|
||||||
{
|
{
|
||||||
@ -99,6 +95,18 @@ export default class HtmlUtils
|
|||||||
form.setAttribute("target", "downloadIframe");
|
form.setAttribute("target", "downloadIframe");
|
||||||
iframe.appendChild(form);
|
iframe.appendChild(form);
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// todo#authHeader - remove after comfortable with sessionUUID //
|
||||||
|
// todo - this could be simplified (i think?) //
|
||||||
|
// it was originally built like this when we had to submit full access token to backend... //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
const authorizationInput = document.createElement("input");
|
||||||
|
authorizationInput.setAttribute("type", "hidden");
|
||||||
|
authorizationInput.setAttribute("id", "authorizationInput");
|
||||||
|
authorizationInput.setAttribute("name", "Authorization");
|
||||||
|
authorizationInput.setAttribute("value", Client.getInstance().getAuthorizationHeaderValue());
|
||||||
|
form.appendChild(authorizationInput);
|
||||||
|
|
||||||
const downloadInput = document.createElement("input");
|
const downloadInput = document.createElement("input");
|
||||||
downloadInput.setAttribute("type", "hidden");
|
downloadInput.setAttribute("type", "hidden");
|
||||||
downloadInput.setAttribute("name", "download");
|
downloadInput.setAttribute("name", "download");
|
||||||
@ -110,16 +118,15 @@ export default class HtmlUtils
|
|||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Open a server-side generated file from a url in a new window (or a data: url)
|
** Open a server-side generated file from a url in a new window (or a data: url)
|
||||||
**
|
|
||||||
** todo - this could be simplified (i think?)
|
|
||||||
** it was originally built like this when we had to submit full access token to backend...
|
|
||||||
**
|
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
static openInNewWindow = (url: string, filename: string) =>
|
static openInNewWindow = (url: string, filename: string) =>
|
||||||
{
|
{
|
||||||
if(url.startsWith("data:"))
|
if(url.startsWith("data:"))
|
||||||
{
|
{
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// todo#authHeader - remove the Authorization input after comfortable with sessionUUID //
|
||||||
|
// todo - this could be simplified (i think?) //
|
||||||
|
// it was originally built like this when we had to submit full access token to backend... //
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
const openInWindow = window.open("", "_blank");
|
const openInWindow = window.open("", "_blank");
|
||||||
openInWindow.document.write(`<html lang="en">
|
openInWindow.document.write(`<html lang="en">
|
||||||
@ -147,6 +154,7 @@ export default class HtmlUtils
|
|||||||
<body>
|
<body>
|
||||||
Opening ${filename}...
|
Opening ${filename}...
|
||||||
<form id="exportForm" method="post" action="${url}" >
|
<form id="exportForm" method="post" action="${url}" >
|
||||||
|
<input type="hidden" name="Authorization" value="${Client.getInstance().getAuthorizationHeaderValue()}">
|
||||||
</form>
|
</form>
|
||||||
</body>
|
</body>
|
||||||
</html>`);
|
</html>`);
|
||||||
|
@ -29,18 +29,11 @@ import {QException} from "@kingsrook/qqq-frontend-core/lib/exceptions/QException
|
|||||||
class Client
|
class Client
|
||||||
{
|
{
|
||||||
private static qController: QController;
|
private static qController: QController;
|
||||||
private static unauthorizedCallback: () => void;
|
|
||||||
|
|
||||||
private static handleException(exception: QException)
|
private static handleException(exception: QException)
|
||||||
{
|
{
|
||||||
|
// todo - check for 401 and clear cookie et al & logout?
|
||||||
console.log(`Caught Exception: ${JSON.stringify(exception)}`);
|
console.log(`Caught Exception: ${JSON.stringify(exception)}`);
|
||||||
|
|
||||||
if(exception && exception.status == "401" && Client.unauthorizedCallback)
|
|
||||||
{
|
|
||||||
console.log("This is a 401 - calling the unauthorized callback.");
|
|
||||||
Client.unauthorizedCallback();
|
|
||||||
}
|
|
||||||
|
|
||||||
throw (exception);
|
throw (exception);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,11 +46,6 @@ class Client
|
|||||||
|
|
||||||
return this.qController;
|
return this.qController;
|
||||||
}
|
}
|
||||||
|
|
||||||
static setUnauthorizedCallback(unauthorizedCallback: () => void)
|
|
||||||
{
|
|
||||||
Client.unauthorizedCallback = unauthorizedCallback;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Client;
|
export default Client;
|
||||||
|
@ -1,11 +1,6 @@
|
|||||||
package com.kingsrook.qqq.materialdashboard.lib;
|
package com.kingsrook.qqq.materialdashboard.lib;
|
||||||
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
|
||||||
import com.kingsrook.qqq.materialdashboard.lib.javalin.QSeleniumJavalin;
|
import com.kingsrook.qqq.materialdashboard.lib.javalin.QSeleniumJavalin;
|
||||||
import io.github.bonigarcia.wdm.WebDriverManager;
|
import io.github.bonigarcia.wdm.WebDriverManager;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
@ -16,7 +11,6 @@ import org.openqa.selenium.Dimension;
|
|||||||
import org.openqa.selenium.WebDriver;
|
import org.openqa.selenium.WebDriver;
|
||||||
import org.openqa.selenium.chrome.ChromeDriver;
|
import org.openqa.selenium.chrome.ChromeDriver;
|
||||||
import org.openqa.selenium.chrome.ChromeOptions;
|
import org.openqa.selenium.chrome.ChromeOptions;
|
||||||
import static org.junit.jupiter.api.Assertions.fail;
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
@ -60,15 +54,7 @@ public class QBaseSeleniumTest
|
|||||||
@BeforeEach
|
@BeforeEach
|
||||||
public void beforeEach()
|
public void beforeEach()
|
||||||
{
|
{
|
||||||
manageDownloadsDirectory();
|
|
||||||
|
|
||||||
HashMap<String, Object> chromePrefs = new HashMap<>();
|
|
||||||
chromePrefs.put("profile.default_content_settings.popups", 0);
|
|
||||||
chromePrefs.put("download.default_directory", getDownloadsDirectory());
|
|
||||||
chromeOptions.setExperimentalOption("prefs", chromePrefs);
|
|
||||||
|
|
||||||
driver = new ChromeDriver(chromeOptions);
|
driver = new ChromeDriver(chromeOptions);
|
||||||
|
|
||||||
driver.manage().window().setSize(new Dimension(1700, 1300));
|
driver.manage().window().setSize(new Dimension(1700, 1300));
|
||||||
qSeleniumLib = new QSeleniumLib(driver);
|
qSeleniumLib = new QSeleniumLib(driver);
|
||||||
|
|
||||||
@ -82,57 +68,6 @@ public class QBaseSeleniumTest
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
**
|
|
||||||
*******************************************************************************/
|
|
||||||
private void manageDownloadsDirectory()
|
|
||||||
{
|
|
||||||
File downloadsDirectory = new File(getDownloadsDirectory());
|
|
||||||
if(!downloadsDirectory.exists())
|
|
||||||
{
|
|
||||||
if(!downloadsDirectory.mkdir())
|
|
||||||
{
|
|
||||||
fail("Could not create downloads directory: " + downloadsDirectory);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!downloadsDirectory.isDirectory())
|
|
||||||
{
|
|
||||||
fail("Downloads directory: " + downloadsDirectory + " is not a directory.");
|
|
||||||
}
|
|
||||||
|
|
||||||
for(File file : CollectionUtils.nonNullArray(downloadsDirectory.listFiles()))
|
|
||||||
{
|
|
||||||
if(!file.delete())
|
|
||||||
{
|
|
||||||
fail("Could not remove a file from the downloads directory: " + file.getAbsolutePath());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
**
|
|
||||||
*******************************************************************************/
|
|
||||||
protected String getDownloadsDirectory()
|
|
||||||
{
|
|
||||||
return ("/tmp/selenium-downloads");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
**
|
|
||||||
*******************************************************************************/
|
|
||||||
protected List<File> getDownloadedFiles()
|
|
||||||
{
|
|
||||||
File[] downloadedFiles = CollectionUtils.nonNullArray((new File(getDownloadsDirectory())).listFiles());
|
|
||||||
return (Arrays.stream(downloadedFiles).toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** control if the test needs to start its own javalin server, or if we're running
|
** control if the test needs to start its own javalin server, or if we're running
|
||||||
** in an environment where an external web server is being used.
|
** in an environment where an external web server is being used.
|
||||||
|
@ -1,24 +1,3 @@
|
|||||||
/*
|
|
||||||
* 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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.kingsrook.qqq.materialdashboard.lib;
|
package com.kingsrook.qqq.materialdashboard.lib;
|
||||||
|
|
||||||
|
|
||||||
@ -27,15 +6,11 @@ import java.time.Duration;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.CompletableFuture;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import com.kingsrook.qqq.backend.core.utils.SleepUtils;
|
|
||||||
import org.apache.commons.io.FileUtils;
|
import org.apache.commons.io.FileUtils;
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
import org.openqa.selenium.By;
|
import org.openqa.selenium.By;
|
||||||
import org.openqa.selenium.JavascriptExecutor;
|
import org.openqa.selenium.JavascriptExecutor;
|
||||||
import org.openqa.selenium.NoSuchElementException;
|
|
||||||
import org.openqa.selenium.OutputType;
|
import org.openqa.selenium.OutputType;
|
||||||
import org.openqa.selenium.StaleElementReferenceException;
|
import org.openqa.selenium.StaleElementReferenceException;
|
||||||
import org.openqa.selenium.WebDriver;
|
import org.openqa.selenium.WebDriver;
|
||||||
@ -61,8 +36,6 @@ public class QSeleniumLib
|
|||||||
private boolean SCREENSHOTS_ENABLED = true;
|
private boolean SCREENSHOTS_ENABLED = true;
|
||||||
private String SCREENSHOTS_PATH = "/tmp/QSeleniumScreenshots/";
|
private String SCREENSHOTS_PATH = "/tmp/QSeleniumScreenshots/";
|
||||||
|
|
||||||
private boolean autoHighlight = false;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
@ -214,13 +187,7 @@ public class QSeleniumLib
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public WebElement waitForSelector(String cssSelector)
|
public WebElement waitForSelector(String cssSelector)
|
||||||
{
|
{
|
||||||
WebElement element = waitForSelectorAll(cssSelector, 1).get(0);
|
return (waitForSelectorAll(cssSelector, 1).get(0));
|
||||||
|
|
||||||
Actions actions = new Actions(driver);
|
|
||||||
actions.moveToElement(element);
|
|
||||||
|
|
||||||
conditionallyAutoHighlight(element);
|
|
||||||
return element;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -263,7 +230,7 @@ public class QSeleniumLib
|
|||||||
do
|
do
|
||||||
{
|
{
|
||||||
List<WebElement> elements = driver.findElements(By.cssSelector(cssSelector));
|
List<WebElement> elements = driver.findElements(By.cssSelector(cssSelector));
|
||||||
if(elements.isEmpty())
|
if(elements.size() == 0)
|
||||||
{
|
{
|
||||||
LOG.debug("Found non-existence of element(s) matching selector [" + cssSelector + "]");
|
LOG.debug("Found non-existence of element(s) matching selector [" + cssSelector + "]");
|
||||||
return;
|
return;
|
||||||
@ -289,7 +256,7 @@ public class QSeleniumLib
|
|||||||
do
|
do
|
||||||
{
|
{
|
||||||
List<WebElement> elements = driver.findElements(By.cssSelector(cssSelector));
|
List<WebElement> elements = driver.findElements(By.cssSelector(cssSelector));
|
||||||
if(elements.isEmpty())
|
if(elements.size() == 0)
|
||||||
{
|
{
|
||||||
LOG.debug("Found non-existence of element(s) matching selector [" + cssSelector + "]");
|
LOG.debug("Found non-existence of element(s) matching selector [" + cssSelector + "]");
|
||||||
return;
|
return;
|
||||||
@ -363,22 +330,6 @@ public class QSeleniumLib
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
**
|
|
||||||
*******************************************************************************/
|
|
||||||
private void soonUnhighlightElement(WebElement element)
|
|
||||||
{
|
|
||||||
CompletableFuture.supplyAsync(() ->
|
|
||||||
{
|
|
||||||
SleepUtils.sleep(2, TimeUnit.SECONDS);
|
|
||||||
JavascriptExecutor js = (JavascriptExecutor) driver;
|
|
||||||
js.executeScript("arguments[0].setAttribute('style', 'background: unset; border: unset;');", element);
|
|
||||||
return (true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
@ -429,10 +380,7 @@ public class QSeleniumLib
|
|||||||
@FunctionalInterface
|
@FunctionalInterface
|
||||||
public interface Code<T>
|
public interface Code<T>
|
||||||
{
|
{
|
||||||
/*******************************************************************************
|
public T run();
|
||||||
**
|
|
||||||
*******************************************************************************/
|
|
||||||
T run();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -482,7 +430,6 @@ public class QSeleniumLib
|
|||||||
LOG.debug("Found element matching selector [" + cssSelector + "] containing text [" + textContains + "].");
|
LOG.debug("Found element matching selector [" + cssSelector + "] containing text [" + textContains + "].");
|
||||||
Actions actions = new Actions(driver);
|
Actions actions = new Actions(driver);
|
||||||
actions.moveToElement(element);
|
actions.moveToElement(element);
|
||||||
conditionallyAutoHighlight(element);
|
|
||||||
return (element);
|
return (element);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -490,10 +437,6 @@ public class QSeleniumLib
|
|||||||
{
|
{
|
||||||
LOG.debug("Caught a StaleElementReferenceException - will retry.");
|
LOG.debug("Caught a StaleElementReferenceException - will retry.");
|
||||||
}
|
}
|
||||||
catch(NoSuchElementException nsee)
|
|
||||||
{
|
|
||||||
LOG.debug("Caught a NoSuchElementException - will retry.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sleepABit();
|
sleepABit();
|
||||||
@ -506,20 +449,6 @@ public class QSeleniumLib
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
**
|
|
||||||
*******************************************************************************/
|
|
||||||
private void conditionallyAutoHighlight(WebElement element)
|
|
||||||
{
|
|
||||||
if(autoHighlight && System.getenv("CIRCLECI") == null)
|
|
||||||
{
|
|
||||||
highlightElement(element);
|
|
||||||
soonUnhighlightElement(element);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Take a screenshot, putting it in the SCREENSHOTS_PATH, with a subdirectory
|
** Take a screenshot, putting it in the SCREENSHOTS_PATH, with a subdirectory
|
||||||
** for the test class simple name, filename = methodName.png.
|
** for the test class simple name, filename = methodName.png.
|
||||||
@ -549,8 +478,7 @@ public class QSeleniumLib
|
|||||||
destFile.mkdirs();
|
destFile.mkdirs();
|
||||||
if(destFile.exists())
|
if(destFile.exists())
|
||||||
{
|
{
|
||||||
String newFileName = destFile.getAbsolutePath().replaceFirst("\\.png", "-" + System.currentTimeMillis() + ".png");
|
destFile.delete();
|
||||||
destFile.renameTo(new File(newFileName));
|
|
||||||
}
|
}
|
||||||
FileUtils.moveFile(outputFile, destFile);
|
FileUtils.moveFile(outputFile, destFile);
|
||||||
LOG.info("Made screenshot at: " + destFile);
|
LOG.info("Made screenshot at: " + destFile);
|
||||||
@ -627,48 +555,4 @@ public class QSeleniumLib
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
**
|
|
||||||
*******************************************************************************/
|
|
||||||
public String getLatestChromeDownloadedFileInfo()
|
|
||||||
{
|
|
||||||
driver.get("chrome://downloads/");
|
|
||||||
JavascriptExecutor js = (JavascriptExecutor) driver;
|
|
||||||
WebElement element = (WebElement) js.executeScript("return document.querySelector('downloads-manager').shadowRoot.querySelector('#mainContainer > iron-list > downloads-item').shadowRoot.querySelector('#content')");
|
|
||||||
return (element.getText());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
** Getter for autoHighlight
|
|
||||||
*******************************************************************************/
|
|
||||||
public boolean getAutoHighlight()
|
|
||||||
{
|
|
||||||
return (this.autoHighlight);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
** Setter for autoHighlight
|
|
||||||
*******************************************************************************/
|
|
||||||
public void setAutoHighlight(boolean autoHighlight)
|
|
||||||
{
|
|
||||||
this.autoHighlight = autoHighlight;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
** Fluent setter for autoHighlight
|
|
||||||
*******************************************************************************/
|
|
||||||
public QSeleniumLib withAutoHighlight(boolean autoHighlight)
|
|
||||||
{
|
|
||||||
this.autoHighlight = autoHighlight;
|
|
||||||
return (this);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -29,9 +29,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
|||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Test that goes to a record, clicks a link for another record, then
|
** Test for Associated Record Scripts functionality.
|
||||||
** hits 'e' on keyboard to edit the second record - and confirms that we're
|
|
||||||
** on the edit url for the second record, not the first (a former bug).
|
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public class ClickLinkOnRecordThenEditShortcutTest extends QBaseSeleniumTest
|
public class ClickLinkOnRecordThenEditShortcutTest extends QBaseSeleniumTest
|
||||||
{
|
{
|
||||||
|
@ -1,111 +0,0 @@
|
|||||||
/*
|
|
||||||
* QQQ - Low-code Application Framework for Engineers.
|
|
||||||
* Copyright (C) 2021-2022. Kingsrook, LLC
|
|
||||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
|
||||||
* contact@kingsrook.com
|
|
||||||
* https://github.com/Kingsrook/
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU Affero General Public License as
|
|
||||||
* published by the Free Software Foundation, either version 3 of the
|
|
||||||
* License, or (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Affero General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.kingsrook.qqq.materialdashboard.tests;
|
|
||||||
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import com.kingsrook.qqq.materialdashboard.lib.QBaseSeleniumTest;
|
|
||||||
import com.kingsrook.qqq.materialdashboard.lib.javalin.QSeleniumJavalin;
|
|
||||||
import org.apache.commons.io.FileUtils;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.openqa.selenium.By;
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
** Tests for dashboard table widget with export button
|
|
||||||
*******************************************************************************/
|
|
||||||
public class DashboardTableWidgetExportTest extends QBaseSeleniumTest
|
|
||||||
{
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
**
|
|
||||||
*******************************************************************************/
|
|
||||||
@Override
|
|
||||||
protected void addJavalinRoutes(QSeleniumJavalin qSeleniumJavalin)
|
|
||||||
{
|
|
||||||
super.addJavalinRoutes(qSeleniumJavalin);
|
|
||||||
qSeleniumJavalin
|
|
||||||
.withRouteToFile("/data/person/count", "data/person/count.json")
|
|
||||||
.withRouteToFile("/data/city/count", "data/city/count.json");
|
|
||||||
|
|
||||||
qSeleniumJavalin.withRouteToString("/widget/SampleTableWidget", """
|
|
||||||
{
|
|
||||||
"label": "Sample Table Widget",
|
|
||||||
"footerHTML": "<span class='material-icons-round notranslate MuiIcon-root MuiIcon-fontSizeInherit' aria-hidden='true'><span class='dashboard-schedule-icon'>schedule</span></span>Updated at 2023-10-17 09:11:38 AM CDT",
|
|
||||||
"columns": [
|
|
||||||
{ "type": "html", "header": "Id", "accessor": "id", "width": "1%" },
|
|
||||||
{ "type": "html", "header": "Name", "accessor": "name", "width": "99%" }
|
|
||||||
],
|
|
||||||
"rows": [
|
|
||||||
{ "id": "1", "name": "<a href='/setup/person/1'>Homer S.</a>" },
|
|
||||||
{ "id": "2", "name": "<a href='/setup/person/2'>Marge B.</a>" },
|
|
||||||
{ "id": "3", "name": "<a href='/setup/person/3'>Bart J.</a>" }
|
|
||||||
],
|
|
||||||
"type": "table"
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
**
|
|
||||||
*******************************************************************************/
|
|
||||||
@Test
|
|
||||||
void testDashboardTableWidgetExport() throws IOException
|
|
||||||
{
|
|
||||||
qSeleniumLib.gotoAndWaitForBreadcrumbHeader("/", "Greetings App");
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////
|
|
||||||
// assert that the table widget rendered its header and some contents //
|
|
||||||
////////////////////////////////////////////////////////////////////////
|
|
||||||
qSeleniumLib.waitForSelectorContaining("#SampleTableWidget h6", "Sample Table Widget");
|
|
||||||
qSeleniumLib.waitForSelectorContaining("#SampleTableWidget table a", "Homer S.");
|
|
||||||
qSeleniumLib.waitForSelectorContaining("#SampleTableWidget div", "Updated at 2023-10-17 09:11:38 AM CDT");
|
|
||||||
|
|
||||||
/////////////////////////////
|
|
||||||
// click the export button //
|
|
||||||
/////////////////////////////
|
|
||||||
qSeleniumLib.waitForSelector("#SampleTableWidget h6")
|
|
||||||
.findElement(By.xpath("./.."))
|
|
||||||
.findElement(By.cssSelector("button"))
|
|
||||||
.click();
|
|
||||||
|
|
||||||
qSeleniumLib.waitForCondition("Should have downloaded 1 file", () -> getDownloadedFiles().size() == 1);
|
|
||||||
File csvFile = getDownloadedFiles().get(0);
|
|
||||||
assertThat(csvFile.getName()).matches("Sample Table Widget.*.csv");
|
|
||||||
String fileContents = FileUtils.readFileToString(csvFile, StandardCharsets.UTF_8);
|
|
||||||
assertEquals("""
|
|
||||||
"Id","Name"
|
|
||||||
"1","Homer S."
|
|
||||||
"2","Marge B."
|
|
||||||
"3","Bart J."
|
|
||||||
""", fileContents);
|
|
||||||
|
|
||||||
// qSeleniumLib.waitForever();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -302,7 +302,8 @@
|
|||||||
"label": "Greetings App",
|
"label": "Greetings App",
|
||||||
"iconName": "emoji_people",
|
"iconName": "emoji_people",
|
||||||
"widgets": [
|
"widgets": [
|
||||||
"SampleTableWidget"
|
"PersonsByCreateDateBarChart",
|
||||||
|
"QuickSightChartRenderer"
|
||||||
],
|
],
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
@ -737,11 +738,139 @@
|
|||||||
"icon": "/kr-icon.png"
|
"icon": "/kr-icon.png"
|
||||||
},
|
},
|
||||||
"widgets": {
|
"widgets": {
|
||||||
"SampleTableWidget": {
|
"parcelTrackingDetails": {
|
||||||
"name": "SampleTableWidget",
|
"name": "parcelTrackingDetails",
|
||||||
"label": "Sample Table Widget",
|
"label": "Tracking Details",
|
||||||
|
"type": "childRecordList"
|
||||||
|
},
|
||||||
|
"deposcoSalesOrderLineItems": {
|
||||||
|
"name": "deposcoSalesOrderLineItems",
|
||||||
|
"label": "Line Items",
|
||||||
|
"type": "childRecordList"
|
||||||
|
},
|
||||||
|
"TotalShipmentsByDayBarChart": {
|
||||||
|
"name": "TotalShipmentsByDayBarChart",
|
||||||
|
"label": "Total Shipments By Day",
|
||||||
|
"type": "chart"
|
||||||
|
},
|
||||||
|
"TotalShipmentsByMonthLineChart": {
|
||||||
|
"name": "TotalShipmentsByMonthLineChart",
|
||||||
|
"label": "Total Shipments By Month",
|
||||||
|
"type": "chart"
|
||||||
|
},
|
||||||
|
"YTDShipmentsByCarrierPieChart": {
|
||||||
|
"name": "YTDShipmentsByCarrierPieChart",
|
||||||
|
"label": "Shipments By Carrier Year To Date",
|
||||||
|
"type": "chart"
|
||||||
|
},
|
||||||
|
"TodaysShipmentsStatisticsCard": {
|
||||||
|
"name": "TodaysShipmentsStatisticsCard",
|
||||||
|
"label": "Today's Shipments",
|
||||||
|
"type": "statistics"
|
||||||
|
},
|
||||||
|
"ShipmentsInTransitStatisticsCard": {
|
||||||
|
"name": "ShipmentsInTransitStatisticsCard",
|
||||||
|
"label": "Shipments In Transit",
|
||||||
|
"type": "statistics"
|
||||||
|
},
|
||||||
|
"OpenOrdersStatisticsCard": {
|
||||||
|
"name": "OpenOrdersStatisticsCard",
|
||||||
|
"label": "Open Orders",
|
||||||
|
"type": "statistics"
|
||||||
|
},
|
||||||
|
"ShippingExceptionsStatisticsCard": {
|
||||||
|
"name": "ShippingExceptionsStatisticsCard",
|
||||||
|
"label": "Shipping Exceptions",
|
||||||
|
"type": "statistics"
|
||||||
|
},
|
||||||
|
"WarehouseLocationCards": {
|
||||||
|
"name": "WarehouseLocationCards",
|
||||||
|
"type": "location"
|
||||||
|
},
|
||||||
|
"TotalShipmentsStatisticsCard": {
|
||||||
|
"name": "TotalShipmentsStatisticsCard",
|
||||||
|
"label": "Total Shipments",
|
||||||
|
"type": "statistics"
|
||||||
|
},
|
||||||
|
"SuccessfulDeliveriesStatisticsCard": {
|
||||||
|
"name": "SuccessfulDeliveriesStatisticsCard",
|
||||||
|
"label": "Successful Deliveries",
|
||||||
|
"type": "statistics"
|
||||||
|
},
|
||||||
|
"ServiceFailuresStatisticsCard": {
|
||||||
|
"name": "ServiceFailuresStatisticsCard",
|
||||||
|
"label": "Service Failures",
|
||||||
|
"type": "statistics"
|
||||||
|
},
|
||||||
|
"CarrierVolumeLineChart": {
|
||||||
|
"name": "CarrierVolumeLineChart",
|
||||||
|
"label": "Carrier Volume By Month",
|
||||||
|
"type": "lineChart"
|
||||||
|
},
|
||||||
|
"YTDSpendByCarrierTable": {
|
||||||
|
"name": "YTDSpendByCarrierTable",
|
||||||
|
"label": "Spend By Carrier Year To Date",
|
||||||
|
"type": "table"
|
||||||
|
},
|
||||||
|
"TimeInTransitBarChart": {
|
||||||
|
"name": "TimeInTransitBarChart",
|
||||||
|
"label": "Time In Transit Last 30 Days",
|
||||||
|
"type": "chart"
|
||||||
|
},
|
||||||
|
"OpenBillingWorksheetsTable": {
|
||||||
|
"name": "OpenBillingWorksheetsTable",
|
||||||
|
"label": "Open Billing Worksheets",
|
||||||
|
"type": "table"
|
||||||
|
},
|
||||||
|
"AssociatedParcelInvoicesTable": {
|
||||||
|
"name": "AssociatedParcelInvoicesTable",
|
||||||
|
"label": "Associated Parcel Invoices",
|
||||||
"type": "table",
|
"type": "table",
|
||||||
"showExportButton": true
|
"icon": "receipt"
|
||||||
|
},
|
||||||
|
"BillingWorksheetLinesTable": {
|
||||||
|
"name": "BillingWorksheetLinesTable",
|
||||||
|
"label": "Billing Worksheet Lines",
|
||||||
|
"type": "table"
|
||||||
|
},
|
||||||
|
"RatingIssuesWidget": {
|
||||||
|
"name": "RatingIssuesWidget",
|
||||||
|
"label": "Rating Issues",
|
||||||
|
"type": "html",
|
||||||
|
"icon": "warning",
|
||||||
|
"gridColumns": 6
|
||||||
|
},
|
||||||
|
"UnassignedParcelInvoicesTable": {
|
||||||
|
"name": "UnassignedParcelInvoicesTable",
|
||||||
|
"label": "Unassigned Parcel Invoices",
|
||||||
|
"type": "table"
|
||||||
|
},
|
||||||
|
"ParcelInvoiceSummaryWidget": {
|
||||||
|
"name": "ParcelInvoiceSummaryWidget",
|
||||||
|
"label": "Parcel Invoice Summary",
|
||||||
|
"type": "multiStatistics"
|
||||||
|
},
|
||||||
|
"ParcelInvoiceLineExceptionsSummaryWidget": {
|
||||||
|
"name": "ParcelInvoiceLineExceptionsSummaryWidget",
|
||||||
|
"label": "Parcel Invoice Line Exceptions",
|
||||||
|
"type": "multiStatistics"
|
||||||
|
},
|
||||||
|
"BillingWorksheetStatusStepper": {
|
||||||
|
"name": "BillingWorksheetStatusStepper",
|
||||||
|
"label": "Billing Worksheet Progress",
|
||||||
|
"type": "stepper",
|
||||||
|
"icon": "refresh",
|
||||||
|
"gridColumns": 6
|
||||||
|
},
|
||||||
|
"PersonsByCreateDateBarChart": {
|
||||||
|
"name": "PersonsByCreateDateBarChart",
|
||||||
|
"label": "Persons By Create Date",
|
||||||
|
"type": "barChart"
|
||||||
|
},
|
||||||
|
"QuickSightChartRenderer": {
|
||||||
|
"name": "QuickSightChartRenderer",
|
||||||
|
"label": "Quick Sight",
|
||||||
|
"type": "quickSightChart"
|
||||||
},
|
},
|
||||||
"scriptViewer": {
|
"scriptViewer": {
|
||||||
"name": "scriptViewer",
|
"name": "scriptViewer",
|
||||||
|
Reference in New Issue
Block a user