@@ -379,7 +413,9 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
+ reloadWidgetCallback={(data) => reloadWidget(i, data)}
+ isChild={areChildren}
+ >
void;
+ showReloadControl: boolean;
isChild?: boolean;
footerHTML?: string;
storeDropdownSelections?: boolean;
@@ -61,6 +67,7 @@ interface Props
Widget.defaultProps = {
isChild: false,
+ showReloadControl: true,
widgetMetaData: {},
widgetData: {},
labelAdditionalComponentsLeft: [],
@@ -68,9 +75,22 @@ Widget.defaultProps = {
};
+interface LabelComponentRenderArgs
+{
+ navigate: NavigateFunction;
+ widgetProps: Props;
+ dropdownData: any[];
+ componentIndex: number;
+ reloadFunction: () => void;
+}
+
+
export class LabelComponent
{
-
+ render = (args: LabelComponentRenderArgs): JSX.Element =>
+ {
+ return (Unsupported component type
)
+ }
}
@@ -86,6 +106,15 @@ export class HeaderLink extends LabelComponent
this.label = label;
this.to = to;
}
+
+ render = (args: LabelComponentRenderArgs): JSX.Element =>
+ {
+ return (
+
+ {this.to ? {this.label} : null}
+
+ );
+ }
}
@@ -97,6 +126,7 @@ export class AddNewRecordButton extends LabelComponent
defaultValues: any;
disabledFields: any;
+
constructor(table: QTableMetaData, defaultValues: any, label: string = "Add new", disabledFields: any = defaultValues)
{
super();
@@ -105,6 +135,45 @@ export class AddNewRecordButton extends LabelComponent
this.defaultValues = defaultValues;
this.disabledFields = disabledFields;
}
+
+ openEditForm = (navigate: any, table: QTableMetaData, id: any = null, defaultValues: any, disabledFields: any) =>
+ {
+ navigate(`#/createChild=${table.name}/defaultValues=${JSON.stringify(defaultValues)}/disabledFields=${JSON.stringify(disabledFields)}`)
+ }
+
+ render = (args: LabelComponentRenderArgs): JSX.Element =>
+ {
+ return (
+
+
+
+ );
+ }
+}
+
+
+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 (
+
+
+
+ );
+ }
}
@@ -121,6 +190,55 @@ export class Dropdown extends LabelComponent
this.options = options;
this.onChangeCallback = onChangeCallback;
}
+
+ render = (args: LabelComponentRenderArgs): JSX.Element =>
+ {
+ let defaultValue = null;
+ const dropdownName = args.widgetProps.widgetData.dropdownNameList[args.componentIndex];
+ const localStorageKey = `${WIDGET_DROPDOWN_SELECTION_LOCAL_STORAGE_KEY_ROOT}.${args.widgetProps.widgetMetaData.name}.${dropdownName}`;
+ if(args.widgetProps.storeDropdownSelections)
+ {
+ ///////////////////////////////////////////////////////////////////////////////////////
+ // see if an existing value is stored in local storage, and if so set it in dropdown //
+ ///////////////////////////////////////////////////////////////////////////////////////
+ defaultValue = JSON.parse(localStorage.getItem(localStorageKey));
+ args.dropdownData[args.componentIndex] = defaultValue?.id;
+ }
+
+ return (
+
+
+
+ );
+ }
+}
+
+
+export class ReloadControl extends LabelComponent
+{
+ callback: () => void;
+
+ constructor(callback: () => void)
+ {
+ super();
+ this.callback = callback;
+ }
+
+ render = (args: LabelComponentRenderArgs): JSX.Element =>
+ {
+ return (
+
+
+
+ );
+ }
}
@@ -132,64 +250,11 @@ function Widget(props: React.PropsWithChildren): JSX.Element
const navigate = useNavigate();
const [dropdownData, setDropdownData] = useState([]);
const [fullScreenWidgetClassName, setFullScreenWidgetClassName] = useState("");
+ const [reloading, setReloading] = useState(false);
- function openEditForm(table: QTableMetaData, id: any = null, defaultValues: any, disabledFields: any)
+ function renderComponent(component: LabelComponent, componentIndex: number)
{
- navigate(`#/createChild=${table.name}/defaultValues=${JSON.stringify(defaultValues)}/disabledFields=${JSON.stringify(disabledFields)}`)
- }
-
- function renderComponent(component: LabelComponent, index: number)
- {
- if(component instanceof HeaderLink)
- {
- const link = component as HeaderLink
- return (
-
- {link.to ? {link.label} : null}
-
- );
- }
-
- if (component instanceof AddNewRecordButton)
- {
- const addNewRecordButton = component as AddNewRecordButton
- return (
-
-
-
- );
- }
-
- if (component instanceof Dropdown)
- {
- let defaultValue = null;
- const dropdownName = props.widgetData.dropdownNameList[index];
- const localStorageKey = `${WIDGET_DROPDOWN_SELECTION_LOCAL_STORAGE_KEY_ROOT}.${props.widgetMetaData.name}.${dropdownName}`;
- if(props.storeDropdownSelections)
- {
- ///////////////////////////////////////////////////////////////////////////////////////
- // see if an existing value is stored in local storage, and if so set it in dropdown //
- ///////////////////////////////////////////////////////////////////////////////////////
- defaultValue = JSON.parse(localStorage.getItem(localStorageKey));
- dropdownData[index] = defaultValue?.id;
- }
-
- const dropdown = component as Dropdown
- return (
-
-
-
- );
- }
-
- return (Unsupported component type.
)
+ return component.render({navigate: navigate, widgetProps: props, dropdownData: dropdownData, componentIndex: componentIndex, reloadFunction: doReload})
}
@@ -209,6 +274,26 @@ function Widget(props: React.PropsWithChildren): JSX.Element
});
}
+ const doReload = () =>
+ {
+ setReloading(true);
+ reloadWidget(dropdownData);
+ }
+
+ useEffect(() =>
+ {
+ setReloading(false);
+ }, [props.widgetData]);
+
+ const effectiveLabelAdditionalComponentsLeft: LabelComponent[] = [];
+ if(props.reloadWidgetCallback && props.widgetData && props.showReloadControl && props.widgetMetaData.showReloadButton)
+ {
+ effectiveLabelAdditionalComponentsLeft.push(new ReloadControl(doReload))
+ }
+ if(props.labelAdditionalComponentsLeft)
+ {
+ props.labelAdditionalComponentsLeft.map((component) => effectiveLabelAdditionalComponentsLeft.push(component));
+ }
function handleDataChange(dropdownLabel: string, changedData: any)
{
@@ -297,9 +382,31 @@ function Widget(props: React.PropsWithChildren): JSX.Element
}
const hasPermission = props.widgetData?.hasPermission === undefined || props.widgetData?.hasPermission === true;
+
+ const isSet = (v: any): boolean =>
+ {
+ return(v !== null && v !== undefined);
+ }
+
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // to avoid taking up the space of the Box with the label and icon and label-components (since it has a height), only output that box if we need any of the components //
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ let needLabelBox = false;
+ if(hasPermission)
+ {
+ needLabelBox ||= (effectiveLabelAdditionalComponentsLeft && effectiveLabelAdditionalComponentsLeft.length > 0);
+ needLabelBox ||= (effectiveLabelAdditionalComponentsRight && effectiveLabelAdditionalComponentsRight.length > 0);
+ needLabelBox ||= isSet(props.widgetMetaData?.icon);
+ needLabelBox ||= isSet(props.widgetData?.label);
+ needLabelBox ||= isSet(props.widgetMetaData?.label);
+ }
+
+ const errorLoading = props.widgetData?.errorLoading !== undefined && props.widgetData?.errorLoading === true;
const widgetContent =
-
+ {
+ needLabelBox &&
+
{
hasPermission ?
@@ -360,14 +467,9 @@ function Widget(props: React.PropsWithChildren): JSX.Element
)
)
}
- {/*
-
- */}
{
hasPermission && (
- props.labelAdditionalComponentsLeft.map((component, i) =>
+ effectiveLabelAdditionalComponentsLeft.map((component, i) =>
{
return ({renderComponent(component, i)});
})
@@ -385,29 +487,44 @@ function Widget(props: React.PropsWithChildren): JSX.Element
}
+ }
{
- hasPermission && props.widgetData?.dropdownNeedsSelectedText ? (
-
-
- {props.widgetData?.dropdownNeedsSelectedText}
-
+ props.widgetMetaData?.isCard && (reloading ? : )
+ }
+ {
+ errorLoading ? (
+
+ error
+ An error occurred loading widget content.
) : (
- hasPermission ? (
- props.children
+ hasPermission && props.widgetData?.dropdownNeedsSelectedText ? (
+
+
+ {props.widgetData?.dropdownNeedsSelectedText}
+
+
) : (
- You do not have permission to view this data.
+ hasPermission ? (
+ props.children
+ ) : (
+ You do not have permission to view this data.
+ )
)
)
}
{
- props?.footerHTML && (
+ ! errorLoading && props?.footerHTML && (
{parse(props.footerHTML)}
)
}
;
- return props.widgetMetaData?.isCard ? {widgetContent} : widgetContent;
+ return props.widgetMetaData?.isCard
+ ?
+ {widgetContent}
+
+ : widgetContent;
}
export default Widget;
diff --git a/src/qqq/components/widgets/misc/RecordGridWidget.tsx b/src/qqq/components/widgets/misc/RecordGridWidget.tsx
index 419934b..6f02ba8 100644
--- a/src/qqq/components/widgets/misc/RecordGridWidget.tsx
+++ b/src/qqq/components/widgets/misc/RecordGridWidget.tsx
@@ -25,9 +25,11 @@ import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
import {DataGridPro, GridCallbackDetails, GridRowParams, MuiEvent} from "@mui/x-data-grid-pro";
import React, {useEffect, useState} from "react";
import {useNavigate} from "react-router-dom";
-import Widget, {AddNewRecordButton, HeaderLink, LabelComponent} from "qqq/components/widgets/Widget";
+import Widget, {AddNewRecordButton, ExportDataButton, HeaderLink, LabelComponent} from "qqq/components/widgets/Widget";
import DataGridUtils from "qqq/utils/DataGridUtils";
+import HtmlUtils from "qqq/utils/HtmlUtils";
import Client from "qqq/utils/qqq/Client";
+import ValueUtils from "qqq/utils/qqq/ValueUtils";
interface Props
{
@@ -42,7 +44,9 @@ const qController = Client.getInstance();
function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element
{
const [rows, setRows] = useState([]);
+ const [records, setRecords] = useState([] as QRecord[])
const [columns, setColumns] = useState([]);
+ const [allColumns, setAllColumns] = useState([])
const navigate = useNavigate();
useEffect(() =>
@@ -68,6 +72,11 @@ function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element
const childTablePath = data.tablePath ? data.tablePath + (data.tablePath.endsWith("/") ? "" : "/") : data.tablePath;
const columns = DataGridUtils.setupGridColumns(tableMetaData, childTablePath, null, "bySection");
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // capture all-columns to use for the export (before we might splice some away from the on-screen display) //
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ setAllColumns(JSON.parse(JSON.stringify(columns)));
+
////////////////////////////////////////////////////////////////
// do not not show the foreign-key column of the parent table //
////////////////////////////////////////////////////////////////
@@ -84,16 +93,67 @@ 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++)
+ {
+ const value = records[i].displayValues.get(allColumns[j].field) ?? records[i].values.get(allColumns[j].field)
+ csv += `${j > 0 ? "," : ""}"${ValueUtils.cleanForCsv(value)}"`
+ }
+ csv += "\n";
+ }
+
+ const fileName = (data?.label ?? widgetMetaData.label) + " " + ValueUtils.formatDateTimeForFileName(new Date()) + ".csv";
+ HtmlUtils.download(fileName, csv);
+ }
+
+ ///////////////////
+ // view all link //
+ ///////////////////
const labelAdditionalComponentsLeft: LabelComponent[] = []
if(data && data.viewAllLink)
{
labelAdditionalComponentsLeft.push(new HeaderLink("View All", data.viewAllLink));
}
+ ///////////////////
+ // export button //
+ ///////////////////
+ let isExportDisabled = true;
+ let tooltipTitle = "Export";
+ if (data && data.childTableMetaData && data.queryOutput && data.queryOutput.records && data.queryOutput.records.length > 0)
+ {
+ isExportDisabled = false;
+
+ if(data.totalRows && data.queryOutput.records.length < data.totalRows)
+ {
+ tooltipTitle = "Export these " + data.queryOutput.records.length + " records."
+ if(data.viewAllLink)
+ {
+ tooltipTitle += "\nClick View All to export all records.";
+ }
+ }
+ }
+
+ labelAdditionalComponentsLeft.push(new ExportDataButton(() => exportCallback(), isExportDisabled, tooltipTitle))
+
+ ////////////////////
+ // add new button //
+ ////////////////////
const labelAdditionalComponentsRight: LabelComponent[] = []
if(data && data.canAddChildRecord)
{
@@ -123,6 +183,7 @@ function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element
return (
diff --git a/src/qqq/components/widgets/tables/TableWidget.tsx b/src/qqq/components/widgets/tables/TableWidget.tsx
new file mode 100644
index 0000000..a9e98c4
--- /dev/null
+++ b/src/qqq/components/widgets/tables/TableWidget.tsx
@@ -0,0 +1,131 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2023. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+
+import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
+// @ts-ignore
+import {htmlToText} from "html-to-text";
+import React, {useEffect, useState} from "react";
+import TableCard from "qqq/components/widgets/tables/TableCard";
+import Widget, {ExportDataButton, WidgetData} from "qqq/components/widgets/Widget";
+import HtmlUtils from "qqq/utils/HtmlUtils";
+import ValueUtils from "qqq/utils/qqq/ValueUtils";
+
+interface Props
+{
+ widgetMetaData?: QWidgetMetaData;
+ widgetData?: WidgetData;
+ reloadWidgetCallback?: (params: string) => void;
+ isChild?: boolean;
+}
+
+TableWidget.defaultProps = {
+};
+
+function TableWidget(props: Props): JSX.Element
+{
+ const [isExportDisabled, setIsExportDisabled] = useState(true);
+
+ const rows = props.widgetData?.rows;
+ const columns = props.widgetData?.columns;
+
+ useEffect(() =>
+ {
+ let isExportDisabled = true;
+ if (props.widgetData && columns && rows && rows.length > 0)
+ {
+ isExportDisabled = false;
+ }
+ setIsExportDisabled(isExportDisabled);
+
+ }, [props.widgetMetaData, props.widgetData]);
+
+ const exportCallback = () =>
+ {
+ if (props.widgetData && rows && columns)
+ {
+ console.log(props.widgetData);
+
+ let csv = "";
+ for (let j = 0; j < columns.length; j++)
+ {
+ if (j > 0)
+ {
+ csv += ",";
+ }
+ csv += `"${columns[j].header}"`;
+ }
+ csv += "\n";
+
+ for (let i = 0; i < rows.length; i++)
+ {
+ for (let j = 0; j < columns.length; j++)
+ {
+ if (j > 0)
+ {
+ csv += ",";
+ }
+
+ const cell = rows[i][columns[j].accessor];
+ const text = htmlToText(cell,
+ {
+ selectors: [
+ {selector: "a", format: "inline"},
+ {selector: ".MuiIcon-root", format: "skip"},
+ {selector: ".button", format: "skip"}
+ ]
+ });
+ csv += `"${ValueUtils.cleanForCsv(text)}"`;
+ }
+ csv += "\n";
+ }
+
+ console.log(csv);
+
+ const fileName = (props.widgetData.label ?? props.widgetMetaData.label) + " " + ValueUtils.formatDateTimeForFileName(new Date()) + ".csv";
+ HtmlUtils.download(fileName, csv);
+ }
+ else
+ {
+ alert("There is no data available to export.");
+ }
+ };
+
+ return (
+ props.reloadWidgetCallback(data)}
+ footerHTML={props.widgetData?.footerHTML}
+ isChild={props.isChild}
+ labelAdditionalComponentsLeft={props.widgetMetaData?.showExportButton ? [new ExportDataButton(() => exportCallback(), isExportDisabled)] : []}
+ >
+
+
+ );
+}
+
+export default TableWidget;
diff --git a/src/qqq/pages/apps/Home.tsx b/src/qqq/pages/apps/Home.tsx
index db4f326..58639d6 100644
--- a/src/qqq/pages/apps/Home.tsx
+++ b/src/qqq/pages/apps/Home.tsx
@@ -26,7 +26,8 @@ import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/
import {QReportMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QReportMetaData";
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
-import {Box, Icon, Typography} from "@mui/material";
+import {Icon, Typography} from "@mui/material";
+import Box from "@mui/material/Box";
import Card from "@mui/material/Card";
import Divider from "@mui/material/Divider";
import Grid from "@mui/material/Grid";
@@ -316,7 +317,7 @@ function AppHome({app}: Props): JSX.Element
{hasTablePermission(tableName) ?
-
+
+ {
+ let csv = `"${ValueUtils.cleanForCsv(fieldMetaData.label)}","Count"\n`;
+ for (let i = 0; i < valueCounts.length; i++)
+ {
+ const fieldValue = valueCounts[i].displayValues.get(fieldMetaData.name);
+ const count = valueCounts[i].values.get("count");
+ csv += `"${ValueUtils.cleanForCsv(fieldValue)}",${count}\n`;
+ }
+
+ const fileName = tableMetaData.label + " - " + fieldMetaData.label + " Column Stats " + ValueUtils.formatDateTimeForFileName(new Date()) + ".csv";
+ HtmlUtils.download(fileName, csv);
+ }
+
function Loading()
{
return (
@@ -200,9 +216,14 @@ function ColumnStats({tableMetaData, fieldMetaData, fieldTableName, filter}: Pro
{statusString ?? <> >}
-
+
+
+
+
diff --git a/src/qqq/styles/qqq-override-styles.css b/src/qqq/styles/qqq-override-styles.css
index 6bdd6a1..cd7bcd1 100644
--- a/src/qqq/styles/qqq-override-styles.css
+++ b/src/qqq/styles/qqq-override-styles.css
@@ -386,16 +386,15 @@ input[type="search"]::-webkit-search-results-decoration { display: none; }
font-size: 0.875rem;
}
-.MuiGrid-root > .MuiBox-root > .material-icons-round,
-.MuiBox-root > .MuiBox-root > .material-icons-round
+.big-icon .material-icons-round
{
font-size: 2rem !important;
}
.dashboard-schedule-icon
{
- font-size: 1rem !important;
+ font-size: 1.1rem !important;
position: relative;
- top: -13px;
- margin-right: 3px;
+ top: -5px;
+ margin-right: 8px;
}
diff --git a/src/qqq/utils/HtmlUtils.ts b/src/qqq/utils/HtmlUtils.ts
index 8b4887e..5197867 100644
--- a/src/qqq/utils/HtmlUtils.ts
+++ b/src/qqq/utils/HtmlUtils.ts
@@ -19,6 +19,9 @@
* along with this program. If not, see .
*/
+/*******************************************************************************
+ ** Utility functions for basic html/webpage/browser things.
+ *******************************************************************************/
export default class HtmlUtils
{
@@ -39,4 +42,21 @@ export default class HtmlUtils
});
};
+ /*******************************************************************************
+ ** Download a client-side generated file (e.g., csv).
+ *******************************************************************************/
+ static download = (filename: string, text: string) =>
+ {
+ var element = document.createElement("a");
+ element.setAttribute("href", "data:text/plain;charset=utf-8," + encodeURIComponent(text));
+ element.setAttribute("download", filename);
+
+ element.style.display = "none";
+ document.body.appendChild(element);
+
+ element.click();
+
+ document.body.removeChild(element);
+ };
+
}
\ No newline at end of file
diff --git a/src/qqq/utils/qqq/FilterUtils.ts b/src/qqq/utils/qqq/FilterUtils.ts
index 7c56bf6..0aebaf7 100644
--- a/src/qqq/utils/qqq/FilterUtils.ts
+++ b/src/qqq/utils/qqq/FilterUtils.ts
@@ -65,7 +65,7 @@ class FilterUtils
return QCriteriaOperator.EQUALS;
case "isNot":
case "!=":
- return QCriteriaOperator.NOT_EQUALS;
+ return QCriteriaOperator.NOT_EQUALS_OR_IS_NULL;
case "after":
case ">":
return QCriteriaOperator.GREATER_THAN;
@@ -138,6 +138,7 @@ class FilterUtils
return ("is");
}
case QCriteriaOperator.NOT_EQUALS:
+ case QCriteriaOperator.NOT_EQUALS_OR_IS_NULL:
if (field.possibleValueSourceName)
{
diff --git a/src/qqq/utils/qqq/ValueUtils.tsx b/src/qqq/utils/qqq/ValueUtils.tsx
index 655c22f..d8c1332 100644
--- a/src/qqq/utils/qqq/ValueUtils.tsx
+++ b/src/qqq/utils/qqq/ValueUtils.tsx
@@ -273,6 +273,14 @@ class ValueUtils
return (`${date.toString("yyyy-MM-ddTHH:mm:ssZ")}`);
}
+ public static formatDateTimeForFileName(date: Date)
+ {
+ const zp = (value: number): string => (value < 10 ? `0${value}` : `${value}`);
+ const d = new Date();
+ const dateString = `${d.getFullYear()}-${zp(d.getMonth() + 1)}-${zp(d.getDate())} ${zp(d.getHours())}${zp(d.getMinutes())}`;
+ return (dateString);
+ }
+
public static getFullWeekday(date: Date)
{
if (!(date instanceof Date))
@@ -410,6 +418,19 @@ class ValueUtils
return toPush;
}
+
+ /*******************************************************************************
+ ** for building CSV in frontends, cleanse null & undefined, and escape "'s
+ *******************************************************************************/
+ public static cleanForCsv(param: any): string
+ {
+ if(param === undefined || param === null)
+ {
+ return ("");
+ }
+
+ return (String(param).replaceAll(/"/g, "\"\""));
+ }
}
////////////////////////////////////////////////////////////////////////////////////////////////