diff --git a/src/qqq/components/widgets/Widget.tsx b/src/qqq/components/widgets/Widget.tsx
index da59312..66e749d 100644
--- a/src/qqq/components/widgets/Widget.tsx
+++ b/src/qqq/components/widgets/Widget.tsx
@@ -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 (
Unsupported component type
)
- }
-}
-
-
-/*******************************************************************************
- **
- *******************************************************************************/
-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 (
-
- {this.to ? {this.label} : null}
-
- );
- }
+ return (Unsupported component type
);
+ };
}
@@ -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
);
- }
-}
-
-
-/*******************************************************************************
- **
- *******************************************************************************/
-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 (
-
-
-
- );
- }
+ };
}
@@ -227,7 +176,7 @@ export class Dropdown extends LabelComponent
/>
);
- }
+ };
}
@@ -251,7 +200,7 @@ export class ReloadControl extends LabelComponent
);
- }
+ };
}
@@ -372,7 +321,7 @@ function Widget(props: React.PropsWithChildren): 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): JSX.Element
}
}
- reloadWidget(dropdownData)
+ reloadWidget(dropdownData);
}
}
@@ -422,7 +371,7 @@ function Widget(props: React.PropsWithChildren): JSX.Element
{
console.log(`No reload widget callback in ${props.widgetMetaData.label}`);
}
- }
+ };
const toggleFullScreenWidget = () =>
{
@@ -434,14 +383,14 @@ function Widget(props: React.PropsWithChildren): 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): 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): JSX.Element
})
)
}
+ {props.labelAdditionalElementsLeft}
{
diff --git a/src/qqq/components/widgets/misc/RecordGridWidget.tsx b/src/qqq/components/widgets/misc/RecordGridWidget.tsx
index 6f02ba8..1f97113 100644
--- a/src/qqq/components/widgets/misc/RecordGridWidget.tsx
+++ b/src/qqq/components/widgets/misc/RecordGridWidget.tsx
@@ -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(
+
+ View All
+
+ )
}
///////////////////
@@ -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(
+
+
+
+ );
+ }
////////////////////
// add new button //
@@ -184,7 +213,7 @@ function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element
- {
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(
+
+
+
+ );
+ }
return (
props.reloadWidgetCallback(data)}
footerHTML={props.widgetData?.footerHTML}
isChild={props.isChild}
- labelAdditionalComponentsLeft={props.widgetMetaData?.showExportButton ? [new ExportDataButton(() => exportCallback(), isExportDisabled)] : []}
+ labelAdditionalElementsLeft={labelAdditionalElementsLeft}
>
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 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.
diff --git a/src/test/java/com/kingsrook/qqq/materialdashboard/lib/QSeleniumLib.java b/src/test/java/com/kingsrook/qqq/materialdashboard/lib/QSeleniumLib.java
index 2b28423..ebd239f 100755
--- a/src/test/java/com/kingsrook/qqq/materialdashboard/lib/QSeleniumLib.java
+++ b/src/test/java/com/kingsrook/qqq/materialdashboard/lib/QSeleniumLib.java
@@ -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 .
+ */
+
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 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 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
{
- 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);
+ }
+
}
diff --git a/src/test/java/com/kingsrook/qqq/materialdashboard/tests/ClickLinkOnRecordThenEditShortcutTest.java b/src/test/java/com/kingsrook/qqq/materialdashboard/tests/ClickLinkOnRecordThenEditShortcutTest.java
index 25ca3c9..741b6ef 100755
--- a/src/test/java/com/kingsrook/qqq/materialdashboard/tests/ClickLinkOnRecordThenEditShortcutTest.java
+++ b/src/test/java/com/kingsrook/qqq/materialdashboard/tests/ClickLinkOnRecordThenEditShortcutTest.java
@@ -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
{
diff --git a/src/test/java/com/kingsrook/qqq/materialdashboard/tests/DashboardTableWidgetExportTest.java b/src/test/java/com/kingsrook/qqq/materialdashboard/tests/DashboardTableWidgetExportTest.java
new file mode 100755
index 0000000..bda8473
--- /dev/null
+++ b/src/test/java/com/kingsrook/qqq/materialdashboard/tests/DashboardTableWidgetExportTest.java
@@ -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 .
+ */
+
+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": "scheduleUpdated 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": "Homer S." },
+ { "id": "2", "name": "Marge B." },
+ { "id": "3", "name": "Bart J." }
+ ],
+ "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();
+ }
+
+}
diff --git a/src/test/resources/fixtures/metaData/index.json b/src/test/resources/fixtures/metaData/index.json
index af09f14..4c076f8 100644
--- a/src/test/resources/fixtures/metaData/index.json
+++ b/src/test/resources/fixtures/metaData/index.json
@@ -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",