From 084ed0732dcbbd12366e21144c9457f7b3a28455 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Wed, 24 May 2023 17:40:30 -0500 Subject: [PATCH 1/2] Support for BLOB, file downloads --- package.json | 2 +- src/qqq/utils/DataGridUtils.tsx | 25 +++++++++++- src/qqq/utils/HtmlUtils.ts | 70 ++++++++++++++++++++++++++++++++ src/qqq/utils/qqq/ValueUtils.tsx | 63 ++++++++++++++++++++++++++-- 4 files changed, 154 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 65b58a2..a3935cc 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "@auth0/auth0-react": "1.10.2", "@emotion/react": "11.7.1", "@emotion/styled": "11.6.0", - "@kingsrook/qqq-frontend-core": "1.0.66", + "@kingsrook/qqq-frontend-core": "1.0.67", "@mui/icons-material": "5.4.1", "@mui/material": "5.11.1", "@mui/styles": "5.11.1", diff --git a/src/qqq/utils/DataGridUtils.tsx b/src/qqq/utils/DataGridUtils.tsx index f43364f..98c57ef 100644 --- a/src/qqq/utils/DataGridUtils.tsx +++ b/src/qqq/utils/DataGridUtils.tsx @@ -168,6 +168,23 @@ export default class DataGridUtils sortedKeys.forEach((key) => { const field = tableMetaData.fields.get(key); + if(field.isHeavy) + { + if(field.type == QFieldType.BLOB) + { + //////////////////////////////////////////////////////// + // assume we DO want heavy blobs - as download links. // + //////////////////////////////////////////////////////// + } + else + { + /////////////////////////////////////////////////// + // otherwise, skip heavy fields on query screen. // + /////////////////////////////////////////////////// + return; + } + } + const column = this.makeColumnFromField(field, tableMetaData, namePrefix, labelPrefix); if(key === tableMetaData.primaryKeyField && linkBase && namePrefix == null) @@ -244,6 +261,7 @@ export default class DataGridUtils const widths: Map = new Map([ ["small", 100], ["medium", 200], + ["medlarge", 300], ["large", 400], ["xlarge", 600] ]); @@ -260,7 +278,7 @@ export default class DataGridUtils let headerName = labelPrefix ? labelPrefix + field.label : field.label; let fieldName = namePrefix ? namePrefix + field.name : field.name; - const column = { + const column: GridColDef = { field: fieldName, type: columnType, headerName: headerName, @@ -269,6 +287,11 @@ export default class DataGridUtils filterOperators: filterOperators, }; + if(field.type == QFieldType.BLOB) + { + column.filterable = false; + } + column.renderCell = (cellValues: any) => ( (cellValues.value) ); diff --git a/src/qqq/utils/HtmlUtils.ts b/src/qqq/utils/HtmlUtils.ts index 5197867..1bf0712 100644 --- a/src/qqq/utils/HtmlUtils.ts +++ b/src/qqq/utils/HtmlUtils.ts @@ -19,6 +19,8 @@ * along with this program. If not, see . */ +import Client from "qqq/utils/qqq/Client"; + /******************************************************************************* ** Utility functions for basic html/webpage/browser things. *******************************************************************************/ @@ -59,4 +61,72 @@ export default class HtmlUtils document.body.removeChild(element); }; + /******************************************************************************* + ** Download a server-side generated file. + *******************************************************************************/ + static downloadUrlViaIFrame = (url: string) => + { + if (document.getElementById("downloadIframe")) + { + document.body.removeChild(document.getElementById("downloadIframe")); + } + + const iframe = document.createElement("iframe"); + iframe.setAttribute("id", "downloadIframe"); + iframe.setAttribute("name", "downloadIframe"); + iframe.style.display = "none"; + // todo - onload event handler to let us know when done? + document.body.appendChild(iframe); + + const form = document.createElement("form"); + form.setAttribute("method", "post"); + form.setAttribute("action", url); + form.setAttribute("target", "downloadIframe"); + iframe.appendChild(form); + + 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"); + downloadInput.setAttribute("type", "hidden"); + downloadInput.setAttribute("name", "download"); + downloadInput.setAttribute("value", "1"); + form.appendChild(downloadInput); + + form.submit(); + }; + + /******************************************************************************* + ** Open a server-side generated file from a url in a new window. + *******************************************************************************/ + static openInNewWindow = (url: string, filename: string) => + { + const openInWindow = window.open("", "_blank"); + openInWindow.document.write(` + + + ${filename} + + + + Opening ${filename}... +
+ +
+ + `); + }; + + } \ No newline at end of file diff --git a/src/qqq/utils/qqq/ValueUtils.tsx b/src/qqq/utils/qqq/ValueUtils.tsx index d8c1332..ea489e2 100644 --- a/src/qqq/utils/qqq/ValueUtils.tsx +++ b/src/qqq/utils/qqq/ValueUtils.tsx @@ -28,11 +28,14 @@ import "datejs"; // https://github.com/datejs/Datejs import {Chip, ClickAwayListener, Icon} from "@mui/material"; import Box from "@mui/material/Box"; import Button from "@mui/material/Button"; +import IconButton from "@mui/material/IconButton"; import Tooltip from "@mui/material/Tooltip"; +import {makeStyles} from "@mui/styles"; import parse from "html-react-parser"; import React, {Fragment, useReducer, useState} from "react"; import AceEditor from "react-ace"; import {Link} from "react-router-dom"; +import HtmlUtils from "qqq/utils/HtmlUtils"; import Client from "qqq/utils/qqq/Client"; /******************************************************************************* @@ -192,6 +195,11 @@ class ValueUtils ); } + if (field.type == QFieldType.BLOB) + { + return (); + } + return (ValueUtils.getUnadornedValueForDisplay(field, rawValue, displayValue)); } @@ -500,9 +508,9 @@ function CodeViewer({name, mode, code}: {name: string; mode: string; code: strin ); } -//////////////////////////////////////////////////////////////////////////////////////////////// -// little private component here, for rendering an AceEditor with some buttons/controls/state // -//////////////////////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////////////// +// little private component here, for rendering "secret-ish" values, that you can click to reveal or copy // +//////////////////////////////////////////////////////////////////////////////////////////////////////////// function RevealComponent({fieldName, value, usage}: {fieldName: string, value: string, usage: string;}): JSX.Element { const [adornmentFieldsMap, setAdornmentFieldsMap] = useState(new Map); @@ -561,7 +569,7 @@ function RevealComponent({fieldName, value, usage}: {fieldName: string, value: s ):( - handleRevealIconClick(e, fieldName)} sx={{cursor: "pointer", fontSize: "15px !important", position: "relative", top: "3px", marginRight: "5px"}}>visibility_off{displayValue} + handleRevealIconClick(e, fieldName)} sx={{cursor: "pointer", fontSize: "15px !important", position: "relative", top: "3px", marginRight: "5px"}}>visibility_off{displayValue} ) ) } @@ -570,5 +578,52 @@ function RevealComponent({fieldName, value, usage}: {fieldName: string, value: s } +interface BlobComponentProps +{ + url: string; + filename: string; +} + +BlobComponent.defaultProps = { + foo: null, +}; + +function BlobComponent({url, filename}: BlobComponentProps): JSX.Element +{ + const download = (event: React.MouseEvent) => + { + event.stopPropagation(); + HtmlUtils.downloadUrlViaIFrame(url); + }; + + const open = (event: React.MouseEvent) => + { + event.stopPropagation(); + HtmlUtils.openInNewWindow(url, filename); + }; + + const useBlobIconStyles = makeStyles({ + blobIcon: { + marginLeft: "0.25rem", + marginRight: "0.25rem", + cursor: "pointer" + } + }) + const classes = useBlobIconStyles(); + + return ( + + {filename} + + open(e)}>open_in_new + + + download(e)}>save_alt + + + ); +} + + export default ValueUtils; From 48ebcb63c09e1b586d2e32704474a62d1aa3b47a Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 30 May 2023 10:20:01 -0500 Subject: [PATCH 2/2] Updates for supporting blobs --- package.json | 2 +- src/qqq/components/audits/AuditBody.tsx | 4 ++ src/qqq/components/forms/DynamicForm.tsx | 29 +++++++++++++- src/qqq/components/forms/EntityForm.tsx | 35 ++++++++++++----- .../records/query/GridFilterOperators.tsx | 14 +++++++ src/qqq/styles/qqq-override-styles.css | 9 ++++- src/qqq/utils/DataGridUtils.tsx | 10 ++--- src/qqq/utils/qqq/ValueUtils.tsx | 39 +++++++++++-------- 8 files changed, 108 insertions(+), 34 deletions(-) diff --git a/package.json b/package.json index a3935cc..df1703b 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "@auth0/auth0-react": "1.10.2", "@emotion/react": "11.7.1", "@emotion/styled": "11.6.0", - "@kingsrook/qqq-frontend-core": "1.0.67", + "@kingsrook/qqq-frontend-core": "1.0.68", "@mui/icons-material": "5.4.1", "@mui/material": "5.11.1", "@mui/styles": "5.11.1", diff --git a/src/qqq/components/audits/AuditBody.tsx b/src/qqq/components/audits/AuditBody.tsx index 072f277..f98d540 100644 --- a/src/qqq/components/audits/AuditBody.tsx +++ b/src/qqq/components/audits/AuditBody.tsx @@ -108,6 +108,10 @@ function AuditBody({tableMetaData, recordId, record}: Props): JSX.Element { return (<>{fieldLabel}: Removed value {(oldValue)}); } + else if(message) + { + return (<>{message}); + } /* const fieldLabel = {tableMetaData?.fields?.get(fieldName)?.label ?? fieldName}; diff --git a/src/qqq/components/forms/DynamicForm.tsx b/src/qqq/components/forms/DynamicForm.tsx index 13eafb3..d98ecd2 100644 --- a/src/qqq/components/forms/DynamicForm.tsx +++ b/src/qqq/components/forms/DynamicForm.tsx @@ -19,15 +19,20 @@ * along with this program. If not, see . */ -import {colors} from "@mui/material"; +import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData"; +import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType"; +import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord"; +import {colors, Icon, InputLabel} from "@mui/material"; import Box from "@mui/material/Box"; import Button from "@mui/material/Button"; import Grid from "@mui/material/Grid"; +import Tooltip from "@mui/material/Tooltip"; import {useFormikContext} from "formik"; import React, {useState} from "react"; import QDynamicFormField from "qqq/components/forms/DynamicFormField"; import DynamicSelect from "qqq/components/forms/DynamicSelect"; import MDTypography from "qqq/components/legacy/MDTypography"; +import ValueUtils from "qqq/utils/qqq/ValueUtils"; interface Props { @@ -35,6 +40,7 @@ interface Props formData: any; bulkEditMode?: boolean; bulkEditSwitchChangeHandler?: any; + record?: QRecord; } function QDynamicForm(props: Props): JSX.Element @@ -60,6 +66,14 @@ function QDynamicForm(props: Props): JSX.Element formikProps.setFieldValue(field.name, event.currentTarget.files[0]); }; + const removeFile = (fieldName: string) => + { + setFileName(null); + formikProps.setFieldValue(fieldName, null); + props.record?.values.delete(fieldName) + props.record?.displayValues.delete(fieldName) + }; + const bulkEditSwitchChanged = (name: string, value: boolean) => { bulkEditSwitchChangeHandler(name, value); @@ -94,10 +108,23 @@ function QDynamicForm(props: Props): JSX.Element if (field.type === "file") { + const pseudoField = new QFieldMetaData({name: fieldName, type: QFieldType.BLOB}); return ( + {field.label} + { + props.record && props.record.values.get(fieldName) && + Current File: + + {ValueUtils.getDisplayValue(pseudoField, props.record, "view")} + + removeFile(fieldName)}>delete + + + + }