mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-17 21:00:45 +00:00
Merge pull request #35 from Kingsrook/bugfix/widget-exports
Bugfix/widget exports
This commit is contained in:
@ -30,7 +30,7 @@ import Tooltip from "@mui/material/Tooltip/Tooltip";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import parse from "html-react-parser";
|
||||
import React, {useEffect, useState} from "react";
|
||||
import {Link, NavigateFunction, useNavigate} from "react-router-dom";
|
||||
import {NavigateFunction, useNavigate} from "react-router-dom";
|
||||
import colors from "qqq/components/legacy/colors";
|
||||
import DropdownMenu, {DropdownOption} from "qqq/components/widgets/components/DropdownMenu";
|
||||
|
||||
@ -46,6 +46,7 @@ export interface WidgetData
|
||||
dropdownNeedsSelectedText?: string;
|
||||
hasPermission?: boolean;
|
||||
errorLoading?: boolean;
|
||||
|
||||
[other: string]: any;
|
||||
}
|
||||
|
||||
@ -53,6 +54,7 @@ export interface WidgetData
|
||||
interface Props
|
||||
{
|
||||
labelAdditionalComponentsLeft: LabelComponent[];
|
||||
labelAdditionalElementsLeft: JSX.Element[];
|
||||
labelAdditionalComponentsRight: LabelComponent[];
|
||||
widgetMetaData?: QWidgetMetaData;
|
||||
widgetData?: WidgetData;
|
||||
@ -70,6 +72,7 @@ Widget.defaultProps = {
|
||||
widgetMetaData: {},
|
||||
widgetData: {},
|
||||
labelAdditionalComponentsLeft: [],
|
||||
labelAdditionalElementsLeft: [],
|
||||
labelAdditionalComponentsRight: [],
|
||||
};
|
||||
|
||||
@ -88,34 +91,8 @@ export class LabelComponent
|
||||
{
|
||||
render = (args: LabelComponentRenderArgs): JSX.Element =>
|
||||
{
|
||||
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>
|
||||
);
|
||||
}
|
||||
return (<div>Unsupported component type</div>);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -141,8 +118,8 @@ export class AddNewRecordButton extends LabelComponent
|
||||
|
||||
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 =>
|
||||
{
|
||||
@ -151,35 +128,7 @@ 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>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -227,7 +176,7 @@ export class Dropdown extends LabelComponent
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -251,7 +200,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>
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -372,7 +321,7 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
||||
|
||||
if (index < 0)
|
||||
{
|
||||
throw(`Could not find table name for label ${tableName}`);
|
||||
throw (`Could not find table name for label ${tableName}`);
|
||||
}
|
||||
|
||||
dropdownData[index] = (changedData) ? changedData.id : null;
|
||||
@ -394,7 +343,7 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
||||
}
|
||||
}
|
||||
|
||||
reloadWidget(dropdownData)
|
||||
reloadWidget(dropdownData);
|
||||
}
|
||||
}
|
||||
|
||||
@ -422,7 +371,7 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
||||
{
|
||||
console.log(`No reload widget callback in ${props.widgetMetaData.label}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const toggleFullScreenWidget = () =>
|
||||
{
|
||||
@ -434,14 +383,14 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
||||
{
|
||||
setFullScreenWidgetClassName("fullScreenWidget");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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 //
|
||||
@ -450,6 +399,7 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
||||
if (hasPermission)
|
||||
{
|
||||
needLabelBox ||= (labelComponentsLeft && labelComponentsLeft.length > 0);
|
||||
needLabelBox ||= (props.labelAdditionalElementsLeft && props.labelAdditionalElementsLeft.length > 0);
|
||||
needLabelBox ||= (labelComponentsRight && labelComponentsRight.length > 0);
|
||||
needLabelBox ||= isSet(props.widgetMetaData?.icon);
|
||||
needLabelBox ||= isSet(props.widgetData?.label);
|
||||
@ -530,6 +480,7 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
||||
})
|
||||
)
|
||||
}
|
||||
{props.labelAdditionalElementsLeft}
|
||||
</Box>
|
||||
<Box>
|
||||
{
|
||||
|
@ -22,10 +22,14 @@
|
||||
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
||||
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
||||
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 React, {useEffect, useState} from "react";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
import Widget, {AddNewRecordButton, ExportDataButton, HeaderLink, LabelComponent} from "qqq/components/widgets/Widget";
|
||||
import {useNavigate, Link} from "react-router-dom";
|
||||
import Widget, {AddNewRecordButton, LabelComponent} from "qqq/components/widgets/Widget";
|
||||
import DataGridUtils from "qqq/utils/DataGridUtils";
|
||||
import HtmlUtils from "qqq/utils/HtmlUtils";
|
||||
import Client from "qqq/utils/qqq/Client";
|
||||
@ -47,6 +51,8 @@ function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element
|
||||
const [records, setRecords] = useState([] as QRecord[])
|
||||
const [columns, setColumns] = useState([]);
|
||||
const [allColumns, setAllColumns] = useState([])
|
||||
const [csv, setCsv] = useState(null as string);
|
||||
const [fileName, setFileName] = useState(null as string);
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() =>
|
||||
@ -75,6 +81,7 @@ 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) //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
const allColumns = [... columns];
|
||||
setAllColumns(JSON.parse(JSON.stringify(columns)));
|
||||
|
||||
////////////////////////////////////////////////////////////////
|
||||
@ -95,39 +102,42 @@ function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element
|
||||
setRows(rows);
|
||||
setRecords(records)
|
||||
setColumns(columns);
|
||||
}
|
||||
}, [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++)
|
||||
let csv = "";
|
||||
for (let i = 0; i < allColumns.length; i++)
|
||||
{
|
||||
const value = records[i].displayValues.get(allColumns[j].field) ?? records[i].values.get(allColumns[j].field)
|
||||
csv += `${j > 0 ? "," : ""}"${ValueUtils.cleanForCsv(value)}"`
|
||||
csv += `${i > 0 ? "," : ""}"${ValueUtils.cleanForCsv(allColumns[i].headerName)}"`
|
||||
}
|
||||
csv += "\n";
|
||||
}
|
||||
|
||||
const fileName = (data?.label ?? widgetMetaData.label) + " " + ValueUtils.formatDateTimeForFileName(new Date()) + ".csv";
|
||||
HtmlUtils.download(fileName, csv);
|
||||
}
|
||||
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";
|
||||
|
||||
setCsv(csv);
|
||||
setFileName(fileName);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
///////////////////
|
||||
// view all link //
|
||||
///////////////////
|
||||
const labelAdditionalComponentsLeft: LabelComponent[] = []
|
||||
const labelAdditionalElementsLeft: JSX.Element[] = [];
|
||||
if(data && data.viewAllLink)
|
||||
{
|
||||
labelAdditionalComponentsLeft.push(new HeaderLink("View All", data.viewAllLink));
|
||||
labelAdditionalElementsLeft.push(
|
||||
<Typography variant="body2" p={2} display="inline" fontSize=".875rem" pt="0" position="relative" top="-0.25rem">
|
||||
<Link to={data.viewAllLink}>View All</Link>
|
||||
</Typography>
|
||||
)
|
||||
}
|
||||
|
||||
///////////////////
|
||||
@ -149,7 +159,26 @@ function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element
|
||||
}
|
||||
}
|
||||
|
||||
labelAdditionalComponentsLeft.push(new ExportDataButton(() => exportCallback(), isExportDisabled, tooltipTitle))
|
||||
const onExportClick = () =>
|
||||
{
|
||||
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 //
|
||||
@ -184,7 +213,7 @@ function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element
|
||||
<Widget
|
||||
widgetMetaData={widgetMetaData}
|
||||
widgetData={data}
|
||||
labelAdditionalComponentsLeft={labelAdditionalComponentsLeft}
|
||||
labelAdditionalElementsLeft={labelAdditionalElementsLeft}
|
||||
labelAdditionalComponentsRight={labelAdditionalComponentsRight}
|
||||
>
|
||||
<DataGridPro
|
||||
|
@ -21,11 +21,15 @@
|
||||
|
||||
|
||||
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
|
||||
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 Widget, {WidgetData} from "qqq/components/widgets/Widget";
|
||||
import HtmlUtils from "qqq/utils/HtmlUtils";
|
||||
import ValueUtils from "qqq/utils/qqq/ValueUtils";
|
||||
|
||||
@ -43,6 +47,8 @@ TableWidget.defaultProps = {
|
||||
function TableWidget(props: Props): JSX.Element
|
||||
{
|
||||
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 columns = props.widgetData?.columns;
|
||||
@ -56,14 +62,8 @@ function TableWidget(props: Props): JSX.Element
|
||||
}
|
||||
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++)
|
||||
{
|
||||
@ -98,16 +98,37 @@ function TableWidget(props: Props): JSX.Element
|
||||
csv += "\n";
|
||||
}
|
||||
|
||||
console.log(csv);
|
||||
setCsv(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);
|
||||
}
|
||||
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 (
|
||||
<Widget
|
||||
@ -116,7 +137,7 @@ function TableWidget(props: Props): JSX.Element
|
||||
reloadWidgetCallback={(data) => props.reloadWidgetCallback(data)}
|
||||
footerHTML={props.widgetData?.footerHTML}
|
||||
isChild={props.isChild}
|
||||
labelAdditionalComponentsLeft={props.widgetMetaData?.showExportButton ? [new ExportDataButton(() => exportCallback(), isExportDisabled)] : []}
|
||||
labelAdditionalElementsLeft={labelAdditionalElementsLeft}
|
||||
>
|
||||
<TableCard
|
||||
noRowsFoundHTML={props.widgetData?.noRowsFoundHTML}
|
||||
|
@ -1,6 +1,11 @@
|
||||
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 io.github.bonigarcia.wdm.WebDriverManager;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
@ -11,6 +16,7 @@ import org.openqa.selenium.Dimension;
|
||||
import org.openqa.selenium.WebDriver;
|
||||
import org.openqa.selenium.chrome.ChromeDriver;
|
||||
import org.openqa.selenium.chrome.ChromeOptions;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -54,7 +60,15 @@ public class QBaseSeleniumTest
|
||||
@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.manage().window().setSize(new Dimension(1700, 1300));
|
||||
qSeleniumLib = new QSeleniumLib(driver);
|
||||
|
||||
@ -68,6 +82,57 @@ 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
|
||||
** in an environment where an external web server is being used.
|
||||
|
@ -1,3 +1,24 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
|
||||
@ -6,11 +27,15 @@ import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
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.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.openqa.selenium.By;
|
||||
import org.openqa.selenium.JavascriptExecutor;
|
||||
import org.openqa.selenium.NoSuchElementException;
|
||||
import org.openqa.selenium.OutputType;
|
||||
import org.openqa.selenium.StaleElementReferenceException;
|
||||
import org.openqa.selenium.WebDriver;
|
||||
@ -36,6 +61,8 @@ public class QSeleniumLib
|
||||
private boolean SCREENSHOTS_ENABLED = true;
|
||||
private String SCREENSHOTS_PATH = "/tmp/QSeleniumScreenshots/";
|
||||
|
||||
private boolean autoHighlight = false;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -187,7 +214,13 @@ public class QSeleniumLib
|
||||
*******************************************************************************/
|
||||
public WebElement waitForSelector(String cssSelector)
|
||||
{
|
||||
return (waitForSelectorAll(cssSelector, 1).get(0));
|
||||
WebElement element = waitForSelectorAll(cssSelector, 1).get(0);
|
||||
|
||||
Actions actions = new Actions(driver);
|
||||
actions.moveToElement(element);
|
||||
|
||||
conditionallyAutoHighlight(element);
|
||||
return element;
|
||||
}
|
||||
|
||||
|
||||
@ -230,7 +263,7 @@ public class QSeleniumLib
|
||||
do
|
||||
{
|
||||
List<WebElement> elements = driver.findElements(By.cssSelector(cssSelector));
|
||||
if(elements.size() == 0)
|
||||
if(elements.isEmpty())
|
||||
{
|
||||
LOG.debug("Found non-existence of element(s) matching selector [" + cssSelector + "]");
|
||||
return;
|
||||
@ -256,7 +289,7 @@ public class QSeleniumLib
|
||||
do
|
||||
{
|
||||
List<WebElement> elements = driver.findElements(By.cssSelector(cssSelector));
|
||||
if(elements.size() == 0)
|
||||
if(elements.isEmpty())
|
||||
{
|
||||
LOG.debug("Found non-existence of element(s) matching selector [" + cssSelector + "]");
|
||||
return;
|
||||
@ -330,6 +363,22 @@ 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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -380,7 +429,10 @@ public class QSeleniumLib
|
||||
@FunctionalInterface
|
||||
public interface Code<T>
|
||||
{
|
||||
public T run();
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
T run();
|
||||
}
|
||||
|
||||
|
||||
@ -430,6 +482,7 @@ public class QSeleniumLib
|
||||
LOG.debug("Found element matching selector [" + cssSelector + "] containing text [" + textContains + "].");
|
||||
Actions actions = new Actions(driver);
|
||||
actions.moveToElement(element);
|
||||
conditionallyAutoHighlight(element);
|
||||
return (element);
|
||||
}
|
||||
}
|
||||
@ -437,6 +490,10 @@ public class QSeleniumLib
|
||||
{
|
||||
LOG.debug("Caught a StaleElementReferenceException - will retry.");
|
||||
}
|
||||
catch(NoSuchElementException nsee)
|
||||
{
|
||||
LOG.debug("Caught a NoSuchElementException - will retry.");
|
||||
}
|
||||
}
|
||||
|
||||
sleepABit();
|
||||
@ -449,6 +506,20 @@ 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
|
||||
** for the test class simple name, filename = methodName.png.
|
||||
@ -478,7 +549,8 @@ public class QSeleniumLib
|
||||
destFile.mkdirs();
|
||||
if(destFile.exists())
|
||||
{
|
||||
destFile.delete();
|
||||
String newFileName = destFile.getAbsolutePath().replaceFirst("\\.png", "-" + System.currentTimeMillis() + ".png");
|
||||
destFile.renameTo(new File(newFileName));
|
||||
}
|
||||
FileUtils.moveFile(outputFile, destFile);
|
||||
LOG.info("Made screenshot at: " + destFile);
|
||||
@ -555,4 +627,48 @@ 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,7 +29,9 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Test for Associated Record Scripts functionality.
|
||||
** Test that goes to a record, clicks a link for another record, then
|
||||
** 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
|
||||
{
|
||||
|
@ -0,0 +1,111 @@
|
||||
/*
|
||||
* 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,8 +302,7 @@
|
||||
"label": "Greetings App",
|
||||
"iconName": "emoji_people",
|
||||
"widgets": [
|
||||
"PersonsByCreateDateBarChart",
|
||||
"QuickSightChartRenderer"
|
||||
"SampleTableWidget"
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
@ -738,139 +737,11 @@
|
||||
"icon": "/kr-icon.png"
|
||||
},
|
||||
"widgets": {
|
||||
"parcelTrackingDetails": {
|
||||
"name": "parcelTrackingDetails",
|
||||
"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",
|
||||
"SampleTableWidget": {
|
||||
"name": "SampleTableWidget",
|
||||
"label": "Sample Table Widget",
|
||||
"type": "table",
|
||||
"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"
|
||||
"showExportButton": true
|
||||
},
|
||||
"scriptViewer": {
|
||||
"name": "scriptViewer",
|
||||
|
Reference in New Issue
Block a user