Compare commits

...

13 Commits

Author SHA1 Message Date
501b8b34c9 Merge branch 'release/0.23.0' 2024-11-22 16:18:57 -06:00
efd1922ee3 Update versions for release 2024-11-22 15:59:20 -06:00
b4f8fb2e18 CE-1946: minor updates to padding, fixes, etc 2024-11-22 11:41:29 -06:00
204025c2a6 Merged feature/CE-1772-generate-labels-poc into dev 2024-11-21 12:31:44 -06:00
6dfc839c30 CE-1946: added child record widget data, minor divider styles 2024-11-19 20:40:16 -06:00
726906061d CE-1946: checkpoint initial commit 2024-11-19 15:07:10 -06:00
2514c463a6 Merge pull request #73 from Kingsrook/feature/CE-1727-mobile-first-uiux
Feature/ce 1727 mobile first uiux
2024-11-12 11:45:46 -06:00
b41a9a6fe6 CE-1960 Pass use-cases through more calls to qController.possibleValues - to fix filter-as-widget dropping possible-value fields w/ a defaultFilter 2024-11-07 10:19:55 -06:00
cfca47054e CE-1727 Remove unused (uncommitted) RevealBlock 2024-11-04 08:57:07 -06:00
b48ef70c5e Update to qqq-frontend-core 1.0.110 2024-11-04 08:52:55 -06:00
81efb7e18d CE-1727 Updates to processes rendering block-widgets, to get up to compatible with the android app 2024-11-04 08:48:46 -06:00
387f09f4ad CE-1772: updated value utils for non blob file downloads 2024-11-03 22:18:27 -06:00
4fd50936ea CE-1727 Add proxy for /qqq/*, for new versioned middleware 2024-10-31 16:07:10 -05:00
26 changed files with 20719 additions and 5009 deletions

24404
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -6,7 +6,7 @@
"@auth0/auth0-react": "1.10.2", "@auth0/auth0-react": "1.10.2",
"@emotion/react": "11.7.1", "@emotion/react": "11.7.1",
"@emotion/styled": "11.6.0", "@emotion/styled": "11.6.0",
"@kingsrook/qqq-frontend-core": "1.0.108", "@kingsrook/qqq-frontend-core": "1.0.110",
"@mui/icons-material": "5.4.1", "@mui/icons-material": "5.4.1",
"@mui/material": "5.11.1", "@mui/material": "5.11.1",
"@mui/styles": "5.11.1", "@mui/styles": "5.11.1",

View File

@ -29,7 +29,7 @@
<packaging>jar</packaging> <packaging>jar</packaging>
<properties> <properties>
<revision>0.23.0-SNAPSHOT</revision> <revision>0.23.0</revision>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

View File

@ -40,6 +40,8 @@ interface Props
value: any; value: any;
type: string; type: string;
isEditable?: boolean; isEditable?: boolean;
placeholder?: string;
backgroundColor?: string;
[key: string]: any; [key: string]: any;
@ -49,7 +51,7 @@ interface Props
} }
function QDynamicFormField({ function QDynamicFormField({
label, name, displayFormat, value, bulkEditMode, bulkEditSwitchChangeHandler, type, isEditable, formFieldObject, ...rest label, name, displayFormat, value, bulkEditMode, bulkEditSwitchChangeHandler, type, isEditable, placeholder, backgroundColor, formFieldObject, ...rest
}: Props): JSX.Element }: Props): JSX.Element
{ {
const [switchChecked, setSwitchChecked] = useState(false); const [switchChecked, setSwitchChecked] = useState(false);
@ -65,18 +67,30 @@ function QDynamicFormField({
inputLabelProps.shrink = true; inputLabelProps.shrink = true;
} }
const inputProps = {}; const inputProps: any = {};
if (displayFormat && displayFormat.startsWith("$")) if (displayFormat && displayFormat.startsWith("$"))
{ {
// @ts-ignore
inputProps.startAdornment = <InputAdornment position="start">$</InputAdornment>; inputProps.startAdornment = <InputAdornment position="start">$</InputAdornment>;
} }
if (displayFormat && displayFormat.endsWith("%%")) if (displayFormat && displayFormat.endsWith("%%"))
{ {
// @ts-ignore
inputProps.endAdornment = <InputAdornment position="end">%</InputAdornment>; inputProps.endAdornment = <InputAdornment position="end">%</InputAdornment>;
} }
if (placeholder)
{
inputProps.placeholder = placeholder
}
if(backgroundColor)
{
inputProps.sx = {
"&.MuiInputBase-root": {
backgroundColor: backgroundColor
}
};
}
// @ts-ignore // @ts-ignore
const handleOnWheel = (e) => const handleOnWheel = (e) =>
{ {

View File

@ -129,8 +129,6 @@ class DynamicFormUtils
const effectivelyIsRequired = field.isRequired && effectiveIsEditable; const effectivelyIsRequired = field.isRequired && effectiveIsEditable;
if (effectivelyIsRequired) if (effectivelyIsRequired)
{
if (field.possibleValueSourceName)
{ {
//////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////
// the "nullable(true)" here doesn't mean that you're allowed to set the field to null... // // the "nullable(true)" here doesn't mean that you're allowed to set the field to null... //
@ -138,11 +136,6 @@ class DynamicFormUtils
//////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////
return (Yup.string().required(`${field.label ?? "This field"} is required.`).nullable(true)); return (Yup.string().required(`${field.label ?? "This field"} is required.`).nullable(true));
} }
else
{
return (Yup.string().required(`${field.label ?? "This field"} is required.`));
}
}
return (null); return (null);
} }

View File

@ -602,7 +602,7 @@ function EntityForm(props: Props): JSX.Element
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
if (fieldMetaData.possibleValueSourceName) if (fieldMetaData.possibleValueSourceName)
{ {
const results: QPossibleValue[] = await qController.possibleValues(tableName, null, fieldName, null, [initialValues[fieldName]]); const results: QPossibleValue[] = await qController.possibleValues(tableName, null, fieldName, null, [initialValues[fieldName]], undefined, "form");
if (results && results.length > 0) if (results && results.length > 0)
{ {
defaultDisplayValues.set(fieldName, results[0].label); defaultDisplayValues.set(fieldName, results[0].label);
@ -818,9 +818,9 @@ function EntityForm(props: Props): JSX.Element
{ {
actions.setSubmitting(true); actions.setSubmitting(true);
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if there's a callback (e.g., for a modal nested on another create/edit screen), then just pass our data back there anre return. // // if there's a callback (e.g., for a modal nested on another create/edit screen), then just pass our data back there and return. //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if (props.onSubmitCallback) if (props.onSubmitCallback)
{ {
props.onSubmitCallback(values); props.onSubmitCallback(values);
@ -1290,7 +1290,7 @@ function EntityForm(props: Props): JSX.Element
table={showEditChildForm.table} table={showEditChildForm.table}
defaultValues={showEditChildForm.defaultValues} defaultValues={showEditChildForm.defaultValues}
disabledFields={showEditChildForm.disabledFields} disabledFields={showEditChildForm.disabledFields}
onSubmitCallback={submitEditChildForm} onSubmitCallback={props.onSubmitCallback ? props.onSubmitCallback : submitEditChildForm}
overrideHeading={`${showEditChildForm.rowIndex != null ? "Editing" : "Creating New"} ${showEditChildForm.table.label}`} overrideHeading={`${showEditChildForm.rowIndex != null ? "Editing" : "Creating New"} ${showEditChildForm.table.label}`}
/> />
</div> </div>

View File

@ -40,16 +40,17 @@ import Snackbar from "@mui/material/Snackbar";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import Tooltip from "@mui/material/Tooltip"; import Tooltip from "@mui/material/Tooltip";
import FormData from "form-data"; import FormData from "form-data";
import React, {useEffect, useReducer, useRef, useState} from "react";
import AceEditor from "react-ace";
import {QCancelButton, QSaveButton} from "qqq/components/buttons/DefaultButtons"; import {QCancelButton, QSaveButton} from "qqq/components/buttons/DefaultButtons";
import DynamicSelect from "qqq/components/forms/DynamicSelect"; import DynamicSelect from "qqq/components/forms/DynamicSelect";
import ScriptDocsForm from "qqq/components/scripts/ScriptDocsForm"; import ScriptDocsForm from "qqq/components/scripts/ScriptDocsForm";
import ScriptTestForm from "qqq/components/scripts/ScriptTestForm"; import ScriptTestForm from "qqq/components/scripts/ScriptTestForm";
import Client from "qqq/utils/qqq/Client"; import Client from "qqq/utils/qqq/Client";
import "ace-builds/src-noconflict/ace";
import "ace-builds/src-noconflict/mode-javascript"; import "ace-builds/src-noconflict/mode-javascript";
import "ace-builds/src-noconflict/theme-github"; import "ace-builds/src-noconflict/theme-github";
import React, {useEffect, useReducer, useRef, useState} from "react";
import AceEditor from "react-ace";
import "ace-builds/src-noconflict/ext-language_tools"; import "ace-builds/src-noconflict/ext-language_tools";
export interface ScriptEditorProps export interface ScriptEditorProps
@ -77,7 +78,7 @@ function buildInitialFileContentsMap(scriptRevisionRecord: QRecord, scriptTypeFi
} }
else else
{ {
let files = scriptRevisionRecord?.associatedRecords?.get("files") let files = scriptRevisionRecord?.associatedRecords?.get("files");
for (let i = 0; i < scriptTypeFileSchemaList.length; i++) for (let i = 0; i < scriptTypeFileSchemaList.length; i++)
{ {
@ -125,21 +126,21 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
{ {
const [closing, setClosing] = useState(false); const [closing, setClosing] = useState(false);
const [apiName, setApiName] = useState(scriptRevisionRecord ? scriptRevisionRecord.values.get("apiName") : null) const [apiName, setApiName] = useState(scriptRevisionRecord ? scriptRevisionRecord.values.get("apiName") : null);
const [apiNameLabel, setApiNameLabel] = useState(scriptRevisionRecord ? scriptRevisionRecord.displayValues.get("apiName") : null) const [apiNameLabel, setApiNameLabel] = useState(scriptRevisionRecord ? scriptRevisionRecord.displayValues.get("apiName") : null);
const [apiVersion, setApiVersion] = useState(scriptRevisionRecord ? scriptRevisionRecord.values.get("apiVersion") : null) const [apiVersion, setApiVersion] = useState(scriptRevisionRecord ? scriptRevisionRecord.values.get("apiVersion") : null);
const [apiVersionLabel, setApiVersionLabel] = useState(scriptRevisionRecord ? scriptRevisionRecord.displayValues.get("apiVersion") : null) const [apiVersionLabel, setApiVersionLabel] = useState(scriptRevisionRecord ? scriptRevisionRecord.displayValues.get("apiVersion") : null);
const fileNamesFromSchema = scriptTypeFileSchemaList.map((schemaRecord) => schemaRecord.values.get("name")) const fileNamesFromSchema = scriptTypeFileSchemaList.map((schemaRecord) => schemaRecord.values.get("name"));
const [availableFileNames, setAvailableFileNames] = useState(fileNamesFromSchema); const [availableFileNames, setAvailableFileNames] = useState(fileNamesFromSchema);
const [openEditorFileNames, setOpenEditorFileNames] = useState([fileNamesFromSchema[0]]) const [openEditorFileNames, setOpenEditorFileNames] = useState([fileNamesFromSchema[0]]);
const [fileContents, setFileContents] = useState(buildInitialFileContentsMap(scriptRevisionRecord, scriptTypeFileSchemaList)) const [fileContents, setFileContents] = useState(buildInitialFileContentsMap(scriptRevisionRecord, scriptTypeFileSchemaList));
const [fileTypes, setFileTypes] = useState(buildFileTypeMap(scriptTypeFileSchemaList)) const [fileTypes, setFileTypes] = useState(buildFileTypeMap(scriptTypeFileSchemaList));
console.log(`file types: ${JSON.stringify(fileTypes)}`); console.log(`file types: ${JSON.stringify(fileTypes)}`);
const [commitMessage, setCommitMessage] = useState("") const [commitMessage, setCommitMessage] = useState("");
const [openTool, setOpenTool] = useState(null); const [openTool, setOpenTool] = useState(null);
const [errorAlert, setErrorAlert] = useState("") const [errorAlert, setErrorAlert] = useState("");
const [promptForCommitMessageOpen, setPromptForCommitMessageOpen] = useState(false); const [promptForCommitMessageOpen, setPromptForCommitMessageOpen] = useState(false);
const [, forceUpdate] = useReducer((x) => x + 1, 0); const [, forceUpdate] = useReducer((x) => x + 1, 0);
const ref = useRef(); const ref = useRef();
@ -241,7 +242,7 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
// need this to make Ace recognize new height. // need this to make Ace recognize new height.
setTimeout(() => setTimeout(() =>
{ {
window.dispatchEvent(new Event("resize")) window.dispatchEvent(new Event("resize"));
}, 100); }, 100);
}; };
@ -249,7 +250,7 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
{ {
if (!apiName || !apiVersion) if (!apiName || !apiVersion)
{ {
setErrorAlert("You must select a value for both API Name and API Version.") setErrorAlert("You must select a value for both API Name and API Version.");
return; return;
} }
@ -278,7 +279,7 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
} }
const fileNamesFromSchema = scriptTypeFileSchemaList.map((schemaRecord) => schemaRecord.values.get("name")) const fileNamesFromSchema = scriptTypeFileSchemaList.map((schemaRecord) => schemaRecord.values.get("name"));
formData.append("fileNames", fileNamesFromSchema.join(",")); formData.append("fileNames", fileNamesFromSchema.join(","));
for (let fileName in fileContents) for (let fileName in fileContents)
@ -299,8 +300,8 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
if (processResult instanceof QJobError) if (processResult instanceof QJobError)
{ {
const jobError = processResult as QJobError const jobError = processResult as QJobError;
setErrorAlert(jobError.userFacingError ?? jobError.error) setErrorAlert(jobError.userFacingError ?? jobError.error);
setClosing(false); setClosing(false);
return; return;
} }
@ -310,28 +311,28 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
catch (e) catch (e)
{ {
// @ts-ignore // @ts-ignore
setErrorAlert(e.message ?? "Unexpected error saving script") setErrorAlert(e.message ?? "Unexpected error saving script");
setClosing(false); setClosing(false);
} }
})(); })();
} };
const cancelClicked = () => const cancelClicked = () =>
{ {
setClosing(true); setClosing(true);
closeCallback(null, "cancelled"); closeCallback(null, "cancelled");
} };
const updateCode = (value: string, event: any, index: number) => const updateCode = (value: string, event: any, index: number) =>
{ {
fileContents[openEditorFileNames[index]] = value; fileContents[openEditorFileNames[index]] = value;
forceUpdate(); forceUpdate();
} };
const updateCommitMessage = (event: React.ChangeEvent<HTMLInputElement>) => const updateCommitMessage = (event: React.ChangeEvent<HTMLInputElement>) =>
{ {
setCommitMessage(event.target.value); setCommitMessage(event.target.value);
} };
const closePromptForCommitMessage = (wasSaveClicked: boolean, message?: string) => const closePromptForCommitMessage = (wasSaveClicked: boolean, message?: string) =>
{ {
@ -339,14 +340,14 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
if (wasSaveClicked) if (wasSaveClicked)
{ {
setCommitMessage(message) setCommitMessage(message);
saveClicked(message); saveClicked(message);
} }
else else
{ {
setClosing(false); setClosing(false);
} }
} };
const changeApiName = (apiNamePossibleValue?: QPossibleValue) => const changeApiName = (apiNamePossibleValue?: QPossibleValue) =>
{ {
@ -358,7 +359,7 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
{ {
setApiName(null); setApiName(null);
} }
} };
const changeApiVersion = (apiVersionPossibleValue?: QPossibleValue) => const changeApiVersion = (apiVersionPossibleValue?: QPossibleValue) =>
{ {
@ -370,33 +371,33 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
{ {
setApiVersion(null); setApiVersion(null);
} }
} };
const handleSelectingFile = (event: SelectChangeEvent, index: number) => const handleSelectingFile = (event: SelectChangeEvent, index: number) =>
{ {
openEditorFileNames[index] = event.target.value openEditorFileNames[index] = event.target.value;
setOpenEditorFileNames(openEditorFileNames); setOpenEditorFileNames(openEditorFileNames);
forceUpdate(); forceUpdate();
} };
const splitEditorClicked = () => const splitEditorClicked = () =>
{ {
openEditorFileNames.push(availableFileNames[0]) openEditorFileNames.push(availableFileNames[0]);
setOpenEditorFileNames(openEditorFileNames); setOpenEditorFileNames(openEditorFileNames);
forceUpdate(); forceUpdate();
} };
const closeEditorClicked = (index: number) => const closeEditorClicked = (index: number) =>
{ {
openEditorFileNames.splice(index, 1) openEditorFileNames.splice(index, 1);
setOpenEditorFileNames(openEditorFileNames); setOpenEditorFileNames(openEditorFileNames);
forceUpdate(); forceUpdate();
} };
const computeEditorWidth = (): string => const computeEditorWidth = (): string =>
{ {
return (100 / openEditorFileNames.length) + "%" return (100 / openEditorFileNames.length) + "%";
} };
return ( return (
<Box className="scriptEditor" sx={{position: "absolute", overflowY: "auto", height: "100%", width: "100%"}} p={6}> <Box className="scriptEditor" sx={{position: "absolute", overflowY: "auto", height: "100%", width: "100%"}} p={6}>
@ -408,7 +409,7 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
{ {
return; return;
} }
setErrorAlert("") setErrorAlert("");
}} anchorOrigin={{vertical: "top", horizontal: "center"}}> }} anchorOrigin={{vertical: "top", horizontal: "center"}}>
<Alert color="error" onClose={() => setErrorAlert("")}> <Alert color="error" onClose={() => setErrorAlert("")}>
{errorAlert} {errorAlert}
@ -535,12 +536,12 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
function CommitMessagePrompt(props: { isOpen: boolean, closeHandler: (wasSaveClicked: boolean, message?: string) => void }) function CommitMessagePrompt(props: { isOpen: boolean, closeHandler: (wasSaveClicked: boolean, message?: string) => void })
{ {
const [commitMessage, setCommitMessage] = useState("No commit message given") const [commitMessage, setCommitMessage] = useState("No commit message given");
const updateCommitMessage = (event: React.ChangeEvent<HTMLInputElement>) => const updateCommitMessage = (event: React.ChangeEvent<HTMLInputElement>) =>
{ {
setCommitMessage(event.target.value); setCommitMessage(event.target.value);
} };
const keyPressHandler = (e: React.KeyboardEvent<HTMLDivElement>) => const keyPressHandler = (e: React.KeyboardEvent<HTMLDivElement>) =>
{ {
@ -548,7 +549,7 @@ function CommitMessagePrompt(props: {isOpen: boolean, closeHandler: (wasSaveClic
{ {
props.closeHandler(true, commitMessage); props.closeHandler(true, commitMessage);
} }
} };
return ( return (
<Dialog <Dialog
@ -582,7 +583,7 @@ function CommitMessagePrompt(props: {isOpen: boolean, closeHandler: (wasSaveClic
<QSaveButton label="Save" onClickHandler={() => props.closeHandler(true, commitMessage)} disabled={false} /> <QSaveButton label="Save" onClickHandler={() => props.closeHandler(true, commitMessage)} disabled={false} />
</DialogActions> </DialogActions>
</Dialog> </Dialog>
) );
} }
export default ScriptEditor; export default ScriptEditor;

View File

@ -22,19 +22,25 @@
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData"; import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
import {Box, Skeleton} from "@mui/material"; import {Box, Skeleton} from "@mui/material";
import Card from "@mui/material/Card";
import Modal from "@mui/material/Modal";
import parse from "html-react-parser"; import parse from "html-react-parser";
import {BlockData} from "qqq/components/widgets/blocks/BlockModels"; import {BlockData} from "qqq/components/widgets/blocks/BlockModels";
import WidgetBlock from "qqq/components/widgets/WidgetBlock"; import WidgetBlock from "qqq/components/widgets/WidgetBlock";
import React from "react"; import ProcessWidgetBlockUtils from "qqq/pages/processes/ProcessWidgetBlockUtils";
import React, {useEffect, useState} from "react";
export interface CompositeData export interface CompositeData
{ {
blockId: string;
blocks: BlockData[]; blocks: BlockData[];
styleOverrides?: any; styleOverrides?: any;
layout?: string; layout?: string;
overlayHtml?: string; overlayHtml?: string;
overlayStyleOverrides?: any; overlayStyleOverrides?: any;
modalMode: string;
styles?: any;
} }
@ -42,14 +48,15 @@ interface CompositeWidgetProps
{ {
widgetMetaData: QWidgetMetaData; widgetMetaData: QWidgetMetaData;
data: CompositeData; data: CompositeData;
actionCallback?: (blockData: BlockData) => boolean; actionCallback?: (blockData: BlockData, eventValues?: { [name: string]: any }) => boolean;
values?: { [key: string]: any };
} }
/******************************************************************************* /*******************************************************************************
** Widget which is a list of Blocks. ** Widget which is a list of Blocks.
*******************************************************************************/ *******************************************************************************/
export default function CompositeWidget({widgetMetaData, data, actionCallback}: CompositeWidgetProps): JSX.Element export default function CompositeWidget({widgetMetaData, data, actionCallback, values}: CompositeWidgetProps): JSX.Element
{ {
if (!data || !data.blocks) if (!data || !data.blocks)
{ {
@ -75,6 +82,12 @@ export default function CompositeWidget({widgetMetaData, data, actionCallback}:
boxStyle.flexWrap = "wrap"; boxStyle.flexWrap = "wrap";
boxStyle.gap = "0.5rem"; boxStyle.gap = "0.5rem";
} }
else if (layout == "FLEX_ROW")
{
boxStyle.display = "flex";
boxStyle.flexDirection = "row";
boxStyle.gap = "0.5rem";
}
else if (layout == "FLEX_ROW_SPACE_BETWEEN") else if (layout == "FLEX_ROW_SPACE_BETWEEN")
{ {
boxStyle.display = "flex"; boxStyle.display = "flex";
@ -114,6 +127,19 @@ export default function CompositeWidget({widgetMetaData, data, actionCallback}:
boxStyle = {...boxStyle, ...data.styleOverrides}; boxStyle = {...boxStyle, ...data.styleOverrides};
} }
if (data.styles?.backgroundColor)
{
boxStyle.backgroundColor = ProcessWidgetBlockUtils.processColorFromStyleMap(data.styles.backgroundColor);
}
if (data.styles?.padding)
{
boxStyle.paddingTop = data.styles?.padding.top + "px"
boxStyle.paddingBottom = data.styles?.padding.bottom + "px"
boxStyle.paddingLeft = data.styles?.padding.left + "px"
boxStyle.paddingRight = data.styles?.padding.right + "px"
}
let overlayStyle: any = {}; let overlayStyle: any = {};
if (data?.overlayStyleOverrides) if (data?.overlayStyleOverrides)
@ -121,7 +147,7 @@ export default function CompositeWidget({widgetMetaData, data, actionCallback}:
overlayStyle = {...overlayStyle, ...data.overlayStyleOverrides}; overlayStyle = {...overlayStyle, ...data.overlayStyleOverrides};
} }
return ( const content = (
<> <>
{ {
data?.overlayHtml && data?.overlayHtml &&
@ -131,7 +157,7 @@ export default function CompositeWidget({widgetMetaData, data, actionCallback}:
{ {
data.blocks.map((block: BlockData, index) => ( data.blocks.map((block: BlockData, index) => (
<React.Fragment key={index}> <React.Fragment key={index}>
<WidgetBlock widgetMetaData={widgetMetaData} block={block} actionCallback={actionCallback} /> <WidgetBlock widgetMetaData={widgetMetaData} block={block} actionCallback={actionCallback} values={values} />
</React.Fragment> </React.Fragment>
)) ))
} }
@ -139,4 +165,53 @@ export default function CompositeWidget({widgetMetaData, data, actionCallback}:
</> </>
); );
if (data.modalMode)
{
const [isModalOpen, setIsModalOpen] = useState(values && (values[data.blockId] == true));
/***************************************************************************
**
***************************************************************************/
const controlCallback = (newValue: boolean) =>
{
setIsModalOpen(newValue);
};
/***************************************************************************
**
***************************************************************************/
const modalOnClose = (event: object, reason: string) =>
{
values[data.blockId] = false;
setIsModalOpen(false);
actionCallback({blockTypeName: "BUTTON", values: {}}, {controlCode: `hideModal:${data.blockId}`});
};
//////////////////////////////////////////////////////////////////////////////////////////
// register the control-callback function - so when buttons are clicked, we can be told //
//////////////////////////////////////////////////////////////////////////////////////////
useEffect(() =>
{
if (actionCallback)
{
actionCallback(null, {
registerControlCallbackName: data.blockId,
registerControlCallbackFunction: controlCallback
});
}
}, []);
return (<Modal open={isModalOpen} onClose={modalOnClose}>
<Box sx={{position: "absolute", overflowY: "auto", maxHeight: "100%", width: "100%"}}>
<Card sx={{my: 5, mx: "auto", p: "1rem", maxWidth: "1024px"}}>
{content}
</Card>
</Box>
</Modal>);
}
else
{
return content;
}
} }

View File

@ -18,15 +18,18 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData"; import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord"; import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
import {Alert, Skeleton} from "@mui/material"; import {Alert, Skeleton} from "@mui/material";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import Grid from "@mui/material/Grid"; import Grid from "@mui/material/Grid";
import Modal from "@mui/material/Modal";
import Tab from "@mui/material/Tab"; import Tab from "@mui/material/Tab";
import Tabs from "@mui/material/Tabs"; import Tabs from "@mui/material/Tabs";
import parse from "html-react-parser"; import parse from "html-react-parser";
import QContext from "QContext"; import QContext from "QContext";
import EntityForm from "qqq/components/forms/EntityForm";
import MDTypography from "qqq/components/legacy/MDTypography"; import MDTypography from "qqq/components/legacy/MDTypography";
import TabPanel from "qqq/components/misc/TabPanel"; import TabPanel from "qqq/components/misc/TabPanel";
import BarChart from "qqq/components/widgets/charts/barchart/BarChart"; import BarChart from "qqq/components/widgets/charts/barchart/BarChart";
@ -43,7 +46,7 @@ import FieldValueListWidget from "qqq/components/widgets/misc/FieldValueListWidg
import FilterAndColumnsSetupWidget from "qqq/components/widgets/misc/FilterAndColumnsSetupWidget"; import FilterAndColumnsSetupWidget from "qqq/components/widgets/misc/FilterAndColumnsSetupWidget";
import PivotTableSetupWidget from "qqq/components/widgets/misc/PivotTableSetupWidget"; import PivotTableSetupWidget from "qqq/components/widgets/misc/PivotTableSetupWidget";
import QuickSightChart from "qqq/components/widgets/misc/QuickSightChart"; import QuickSightChart from "qqq/components/widgets/misc/QuickSightChart";
import RecordGridWidget from "qqq/components/widgets/misc/RecordGridWidget"; import RecordGridWidget, {ChildRecordListData} from "qqq/components/widgets/misc/RecordGridWidget";
import ScriptViewer from "qqq/components/widgets/misc/ScriptViewer"; import ScriptViewer from "qqq/components/widgets/misc/ScriptViewer";
import StepperCard from "qqq/components/widgets/misc/StepperCard"; import StepperCard from "qqq/components/widgets/misc/StepperCard";
import USMapWidget from "qqq/components/widgets/misc/USMapWidget"; import USMapWidget from "qqq/components/widgets/misc/USMapWidget";
@ -71,6 +74,9 @@ interface Props
childUrlParams?: string; childUrlParams?: string;
parentWidgetMetaData?: QWidgetMetaData; parentWidgetMetaData?: QWidgetMetaData;
wrapWidgetsInTabPanels: boolean; wrapWidgetsInTabPanels: boolean;
actionCallback?: (data: any, eventValues?: { [name: string]: any }) => boolean;
initialWidgetDataList: any[];
values?: { [key: string]: any };
} }
DashboardWidgets.defaultProps = { DashboardWidgets.defaultProps = {
@ -82,11 +88,14 @@ DashboardWidgets.defaultProps = {
childUrlParams: "", childUrlParams: "",
parentWidgetMetaData: null, parentWidgetMetaData: null,
wrapWidgetsInTabPanels: false, wrapWidgetsInTabPanels: false,
actionCallback: null,
initialWidgetDataList: null,
values: {}
}; };
function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, record, omitWrappingGridContainer, areChildren, childUrlParams, parentWidgetMetaData, wrapWidgetsInTabPanels}: Props): JSX.Element function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, record, omitWrappingGridContainer, areChildren, childUrlParams, parentWidgetMetaData, wrapWidgetsInTabPanels, actionCallback, initialWidgetDataList, values}: Props): JSX.Element
{ {
const [widgetData, setWidgetData] = useState([] as any[]); const [widgetData, setWidgetData] = useState(initialWidgetDataList == null ? [] as any[] : initialWidgetDataList);
const [widgetCounter, setWidgetCounter] = useState(0); const [widgetCounter, setWidgetCounter] = useState(0);
const [, forceUpdate] = useReducer((x) => x + 1, 0); const [, forceUpdate] = useReducer((x) => x + 1, 0);
@ -94,6 +103,11 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
const [haveLoadedParams, setHaveLoadedParams] = useState(false); const [haveLoadedParams, setHaveLoadedParams] = useState(false);
const {accentColor} = useContext(QContext); const {accentColor} = useContext(QContext);
/////////////////////////
// modal form controls //
/////////////////////////
const [showEditChildForm, setShowEditChildForm] = useState(null as any);
let initialSelectedTab = 0; let initialSelectedTab = 0;
let selectedTabKey: string = null; let selectedTabKey: string = null;
if (parentWidgetMetaData && wrapWidgetsInTabPanels) if (parentWidgetMetaData && wrapWidgetsInTabPanels)
@ -114,7 +128,15 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
useEffect(() => useEffect(() =>
{ {
if (initialWidgetDataList && initialWidgetDataList.length > 0)
{
// todo actually, should this check each element of the array, down in the loop? yeah, when we need to, do it that way.
console.log("We already have initial widget data, so not fetching from backend.");
return;
}
setWidgetData([]); setWidgetData([]);
for (let i = 0; i < widgetMetaDataList.length; i++) for (let i = 0; i < widgetMetaDataList.length; i++)
{ {
const widgetMetaData = widgetMetaDataList[i]; const widgetMetaData = widgetMetaDataList[i];
@ -151,7 +173,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
const reloadWidget = async (index: number, data: string) => const reloadWidget = async (index: number, data: string) =>
{ {
(async () => await (async () =>
{ {
const urlParams = getQueryParams(widgetMetaDataList[index], data); const urlParams = getQueryParams(widgetMetaDataList[index], data);
setCurrentUrlParams(urlParams); setCurrentUrlParams(urlParams);
@ -270,6 +292,150 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
return (rs); return (rs);
} }
/*******************************************************************************
**
*******************************************************************************/
const closeEditChildForm = (event: object, reason: string) =>
{
if (reason === "backdropClick" || reason === "escapeKeyDown")
{
return;
}
setShowEditChildForm(null);
};
/*******************************************************************************
**
*******************************************************************************/
function deleteChildRecord(name: string, widgetIndex: number, rowIndex: number)
{
updateChildRecordList(name, "delete", rowIndex);
actionCallback(widgetData[widgetIndex]);
};
/*******************************************************************************
**
*******************************************************************************/
function openEditChildRecord(name: string, widgetData: any, rowIndex: number)
{
let defaultValues = widgetData.queryOutput.records[rowIndex].values;
let disabledFields = widgetData.disabledFieldsForNewChildRecords;
if (!disabledFields)
{
disabledFields = widgetData.defaultValuesForNewChildRecords;
}
doOpenEditChildForm(name, widgetData.childTableMetaData, rowIndex, defaultValues, disabledFields);
}
/*******************************************************************************
**
*******************************************************************************/
function openAddChildRecord(name: string, widgetData: any)
{
let defaultValues = widgetData.defaultValuesForNewChildRecords;
let disabledFields = widgetData.disabledFieldsForNewChildRecords;
if (!disabledFields)
{
disabledFields = widgetData.defaultValuesForNewChildRecords;
}
doOpenEditChildForm(name, widgetData.childTableMetaData, null, defaultValues, disabledFields);
}
/*******************************************************************************
**
*******************************************************************************/
function doOpenEditChildForm(widgetName: string, table: QTableMetaData, rowIndex: number, defaultValues: any, disabledFields: any)
{
const showEditChildForm: any = {};
showEditChildForm.widgetName = widgetName;
showEditChildForm.table = table;
showEditChildForm.rowIndex = rowIndex;
showEditChildForm.defaultValues = defaultValues;
showEditChildForm.disabledFields = disabledFields;
setShowEditChildForm(showEditChildForm);
}
/*******************************************************************************
**
*******************************************************************************/
function submitEditChildForm(values: any)
{
updateChildRecordList(showEditChildForm.widgetName, showEditChildForm.rowIndex == null ? "insert" : "edit", showEditChildForm.rowIndex, values);
let widgetIndex = determineChildRecordListIndex(showEditChildForm.widgetName);
actionCallback(widgetData[widgetIndex]);
}
/*******************************************************************************
**
*******************************************************************************/
function determineChildRecordListIndex(widgetName: string): number
{
let widgetIndex = -1;
for (var i = 0; i < widgetMetaDataList.length; i++)
{
const widgetMetaData = widgetMetaDataList[i];
if (widgetMetaData.name == widgetName)
{
widgetIndex = i;
break;
}
}
return (widgetIndex);
}
/*******************************************************************************
**
*******************************************************************************/
function updateChildRecordList(widgetName: string, action: "insert" | "edit" | "delete", rowIndex?: number, values?: any)
{
////////////////////////////////////////////////
// find the correct child record widget index //
////////////////////////////////////////////////
let widgetIndex = determineChildRecordListIndex(widgetName);
if (!widgetData[widgetIndex].queryOutput.records)
{
widgetData[widgetIndex].queryOutput.records = [];
}
const newChildListWidgetData: ChildRecordListData = widgetData[widgetIndex];
if (!newChildListWidgetData.queryOutput.records)
{
newChildListWidgetData.queryOutput.records = [];
}
switch (action)
{
case "insert":
newChildListWidgetData.queryOutput.records.push({values: values});
break;
case "edit":
newChildListWidgetData.queryOutput.records[rowIndex] = {values: values};
break;
case "delete":
newChildListWidgetData.queryOutput.records.splice(rowIndex, 1);
break;
}
newChildListWidgetData.totalRows = newChildListWidgetData.queryOutput.records.length;
widgetData[widgetIndex] = newChildListWidgetData;
setWidgetData(widgetData);
setShowEditChildForm(null);
}
const renderWidget = (widgetMetaData: QWidgetMetaData, i: number): JSX.Element => const renderWidget = (widgetMetaData: QWidgetMetaData, i: number): JSX.Element =>
{ {
const labelAdditionalComponentsRight: LabelComponent[] = []; const labelAdditionalComponentsRight: LabelComponent[] = [];
@ -309,7 +475,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
) )
} }
{ {
widgetMetaData.type === "alert" && widgetData[i]?.html && ( widgetMetaData.type === "alert" && widgetData[i]?.html && !widgetData[i]?.hideWidget && (
<Widget <Widget
omitPadding={true} omitPadding={true}
widgetMetaData={widgetMetaData} widgetMetaData={widgetMetaData}
@ -319,7 +485,16 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
labelAdditionalComponentsRight={labelAdditionalComponentsRight} labelAdditionalComponentsRight={labelAdditionalComponentsRight}
labelAdditionalComponentsLeft={labelAdditionalComponentsLeft} labelAdditionalComponentsLeft={labelAdditionalComponentsLeft}
> >
<Alert severity={widgetData[i]?.alertType?.toLowerCase()}>{parse(widgetData[i]?.html)}</Alert> <Alert severity={widgetData[i]?.alertType?.toLowerCase()}>
{parse(widgetData[i]?.html)}
{widgetData[i]?.bulletList && (
<div style={{fontSize: "14px"}}>
{widgetData[i].bulletList.map((bullet: string, index: number) =>
<li key={`widget-${i}-${index}`}>{parse(bullet)}</li>
)}
</div>
)}
</Alert>
</Widget> </Widget>
) )
} }
@ -501,9 +676,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
} }
{ {
widgetMetaData.type === "divider" && ( widgetMetaData.type === "divider" && (
<Box>
<DividerWidget /> <DividerWidget />
</Box>
) )
} }
{ {
@ -537,6 +710,12 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
widgetMetaData.type === "childRecordList" && ( widgetMetaData.type === "childRecordList" && (
widgetData && widgetData[i] && widgetData && widgetData[i] &&
<RecordGridWidget <RecordGridWidget
disableRowClick={widgetData[i]?.disableRowClick}
allowRecordEdit={widgetData[i]?.allowRecordEdit}
allowRecordDelete={widgetData[i]?.allowRecordDelete}
deleteRecordCallback={(rowIndex) => deleteChildRecord(widgetMetaData.name, i, rowIndex)}
editRecordCallback={(rowIndex) => openEditChildRecord(widgetMetaData.name, widgetData[i], rowIndex)}
addNewRecordCallback={widgetData[i]?.isInProcess ? () => openAddChildRecord(widgetMetaData.name, widgetData[i]) : null}
widgetMetaData={widgetMetaData} widgetMetaData={widgetMetaData}
data={widgetData[i]} data={widgetData[i]}
/> />
@ -563,7 +742,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
labelAdditionalComponentsRight={labelAdditionalComponentsRight} labelAdditionalComponentsRight={labelAdditionalComponentsRight}
labelAdditionalComponentsLeft={labelAdditionalComponentsLeft} labelAdditionalComponentsLeft={labelAdditionalComponentsLeft}
> >
<CompositeWidget widgetMetaData={widgetMetaData} data={widgetData[i]} /> <CompositeWidget widgetMetaData={widgetMetaData} data={widgetData[i]} actionCallback={actionCallback} values={values} />
</Widget> </Widget>
) )
} }
@ -642,7 +821,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
for (let size of ["xs", "sm", "md", "lg", "xl", "xxl"]) for (let size of ["xs", "sm", "md", "lg", "xl", "xxl"])
{ {
const key = `gridCols:sizeClass:${size}` const key = `gridCols:sizeClass:${size}`;
if (widgetMetaData?.defaultValues?.has(key)) if (widgetMetaData?.defaultValues?.has(key))
{ {
gridProps[size] = widgetMetaData?.defaultValues.get(key); gridProps[size] = widgetMetaData?.defaultValues.get(key);
@ -710,6 +889,22 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
</Grid> </Grid>
) )
} }
{
showEditChildForm &&
<Modal open={showEditChildForm as boolean} onClose={(event, reason) => closeEditChildForm(event, reason)}>
<div className="modalEditForm">
<EntityForm
isModal={true}
closeModalHandler={closeEditChildForm}
table={showEditChildForm.table}
defaultValues={showEditChildForm.defaultValues}
disabledFields={showEditChildForm.disabledFields}
onSubmitCallback={submitEditChildForm}
overrideHeading={`${showEditChildForm.rowIndex != null ? "Editing" : "Creating New"} ${showEditChildForm.table.label}`}
/>
</div>
</Modal>
}
</> </>
) : null ) : null
); );

View File

@ -22,7 +22,7 @@
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData"; import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
import {Alert, Skeleton} from "@mui/material"; import {Alert, Skeleton} from "@mui/material";
import ActionButtonBlock from "qqq/components/widgets/blocks/ActionButtonBlock"; import ButtonBlock from "qqq/components/widgets/blocks/ButtonBlock";
import AudioBlock from "qqq/components/widgets/blocks/AudioBlock"; import AudioBlock from "qqq/components/widgets/blocks/AudioBlock";
import InputFieldBlock from "qqq/components/widgets/blocks/InputFieldBlock"; import InputFieldBlock from "qqq/components/widgets/blocks/InputFieldBlock";
import React from "react"; import React from "react";
@ -42,14 +42,15 @@ interface WidgetBlockProps
{ {
widgetMetaData: QWidgetMetaData; widgetMetaData: QWidgetMetaData;
block: BlockData; block: BlockData;
actionCallback?: (blockData: BlockData) => boolean; actionCallback?: (blockData: BlockData, eventValues?: {[name: string]: any}) => boolean;
values?: { [key: string]: any };
} }
/******************************************************************************* /*******************************************************************************
** Component to render a single Block in the widget framework! ** Component to render a single Block in the widget framework!
*******************************************************************************/ *******************************************************************************/
export default function WidgetBlock({widgetMetaData, block, actionCallback}: WidgetBlockProps): JSX.Element export default function WidgetBlock({widgetMetaData, block, actionCallback, values}: WidgetBlockProps): JSX.Element
{ {
if(!block) if(!block)
{ {
@ -69,7 +70,7 @@ export default function WidgetBlock({widgetMetaData, block, actionCallback}: Wid
if(block.blockTypeName == "COMPOSITE") if(block.blockTypeName == "COMPOSITE")
{ {
// @ts-ignore - special case for composite type block... // @ts-ignore - special case for composite type block...
return (<CompositeWidget widgetMetaData={widgetMetaData} data={block} actionCallback={actionCallback} />); return (<CompositeWidget widgetMetaData={widgetMetaData} data={block} actionCallback={actionCallback} values={values} />);
} }
switch(block.blockTypeName) switch(block.blockTypeName)
@ -90,8 +91,8 @@ export default function WidgetBlock({widgetMetaData, block, actionCallback}: Wid
return (<BigNumberBlock widgetMetaData={widgetMetaData} data={block} />); return (<BigNumberBlock widgetMetaData={widgetMetaData} data={block} />);
case "INPUT_FIELD": case "INPUT_FIELD":
return (<InputFieldBlock widgetMetaData={widgetMetaData} data={block} actionCallback={actionCallback} />); return (<InputFieldBlock widgetMetaData={widgetMetaData} data={block} actionCallback={actionCallback} />);
case "ACTION_BUTTON": case "BUTTON":
return (<ActionButtonBlock widgetMetaData={widgetMetaData} data={block} actionCallback={actionCallback} />); return (<ButtonBlock widgetMetaData={widgetMetaData} data={block} actionCallback={actionCallback} />);
case "AUDIO": case "AUDIO":
return (<AudioBlock widgetMetaData={widgetMetaData} data={block} />); return (<AudioBlock widgetMetaData={widgetMetaData} data={block} />);
case "IMAGE": case "IMAGE":

View File

@ -29,30 +29,56 @@ import React from "react";
/******************************************************************************* /*******************************************************************************
** Block that renders ... an action button... ** Block that renders ... a button...
** **
*******************************************************************************/ *******************************************************************************/
export default function ActionButtonBlock({widgetMetaData, data, actionCallback}: StandardBlockComponentProps): JSX.Element export default function ButtonBlock({widgetMetaData, data, actionCallback}: StandardBlockComponentProps): JSX.Element
{ {
const icon = data.values.iconName ? <Icon>{data.values.iconName}</Icon> : null; const startIcon = data.values.startIcon?.name ? <Icon>{data.values.startIcon.name}</Icon> : null;
const endIcon = data.values.endIcon?.name ? <Icon>{data.values.endIcon.name}</Icon> : null;
function onClick() function onClick()
{ {
if (actionCallback) if (actionCallback)
{ {
actionCallback(data, {actionCode: data.values?.actionCode}) actionCallback(data, data.values);
} }
else else
{ {
console.log("ActionButtonBlock onClick with no actionCallback present, so, noop"); console.log("ButtonBlock onClick with no actionCallback present, so, noop");
} }
} }
let buttonVariant: "gradient" | "outlined" | "text" = "gradient";
if (data.styles?.format == "outlined")
{
buttonVariant = "outlined";
}
else if (data.styles?.format == "text")
{
buttonVariant = "text";
}
else if (data.styles?.format == "filled")
{
buttonVariant = "gradient";
}
// todo - button colors... but to do RGB's, might need to move away from MDButton?
return ( return (
<BlockElementWrapper metaData={widgetMetaData} data={data} slot=""> <BlockElementWrapper metaData={widgetMetaData} data={data} slot="">
<Box mx={1} my={1} minWidth={standardWidth}> <Box mx={1} my={1} minWidth={standardWidth}>
<MDButton type="button" variant="gradient" color="dark" size="small" fullWidth startIcon={icon} onClick={onClick}> <MDButton
{data.values.label ?? "Action"} type="button"
variant={buttonVariant}
color="dark"
size="small"
fullWidth
startIcon={startIcon}
endIcon={endIcon}
onClick={onClick}
>
{data.values.label ?? "Button"}
</MDButton> </MDButton>
</Box> </Box>
</BlockElementWrapper> </BlockElementWrapper>

View File

@ -52,10 +52,10 @@ export default function InputFieldBlock({widgetMetaData, data, actionCallback}:
// so let us remove the default blur handler, for the first (auto) focus/blur // // so let us remove the default blur handler, for the first (auto) focus/blur //
// cycle, and we seem to have a better time. // // cycle, and we seem to have a better time. //
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
let onBlurRest: {onBlur?: any} = {} let dynamicFormFieldRest: {onBlur?: any, sx?: any} = {}
if(autoFocus && blurCount == 0) if(autoFocus && blurCount == 0)
{ {
onBlurRest.onBlur = (event: React.SyntheticEvent) => dynamicFormFieldRest.onBlur = (event: React.SyntheticEvent) =>
{ {
event.stopPropagation(); event.stopPropagation();
event.preventDefault(); event.preventDefault();
@ -120,7 +120,18 @@ export default function InputFieldBlock({widgetMetaData, data, actionCallback}:
<BlockElementWrapper metaData={widgetMetaData} data={data} slot=""> <BlockElementWrapper metaData={widgetMetaData} data={data} slot="">
<> <>
{labelElement} {labelElement}
<QDynamicFormField name={fieldMetaData.name} displayFormat={null} label="" formFieldObject={dynamicField} type={fieldMetaData.type} value={value} autoFocus={autoFocus} onKeyUp={eventHandler} {...onBlurRest} /> <QDynamicFormField
name={fieldMetaData.name}
displayFormat={null}
label=""
placeholder={data.values?.placeholder}
backgroundColor="#FFFFFF"
formFieldObject={dynamicField}
type={fieldMetaData.type}
value={value}
autoFocus={autoFocus}
onKeyUp={eventHandler}
{...dynamicFormFieldRest} />
</> </>
</BlockElementWrapper> </BlockElementWrapper>
</Box> </Box>

View File

@ -20,9 +20,11 @@
*/ */
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import Icon from "@mui/material/Icon";
import BlockElementWrapper from "qqq/components/widgets/blocks/BlockElementWrapper"; import BlockElementWrapper from "qqq/components/widgets/blocks/BlockElementWrapper";
import {StandardBlockComponentProps} from "qqq/components/widgets/blocks/BlockModels"; import {StandardBlockComponentProps} from "qqq/components/widgets/blocks/BlockModels";
import DumpJsonBox from "qqq/utils/DumpJsonBox"; import ProcessWidgetBlockUtils from "qqq/pages/processes/ProcessWidgetBlockUtils";
import React from "react";
/******************************************************************************* /*******************************************************************************
** Block that renders ... just some text. ** Block that renders ... just some text.
@ -32,30 +34,13 @@ import DumpJsonBox from "qqq/utils/DumpJsonBox";
export default function TextBlock({widgetMetaData, data}: StandardBlockComponentProps): JSX.Element export default function TextBlock({widgetMetaData, data}: StandardBlockComponentProps): JSX.Element
{ {
let color = "rgba(0, 0, 0, 0.87)"; let color = "rgba(0, 0, 0, 0.87)";
if (data.styles?.standardColor) if (data.styles?.color)
{ {
switch (data.styles?.standardColor) color = ProcessWidgetBlockUtils.processColorFromStyleMap(data.styles.color);
{
case "SUCCESS":
color = "#2BA83F";
break;
case "WARNING":
color = "#FBA132";
break;
case "ERROR":
color = "#FB4141";
break;
case "INFO":
color = "#458CFF";
break;
case "MUTED":
color = "#7b809a";
break;
}
} }
let boxStyle = {}; let boxStyle = {};
if (data.styles?.isAlert) if (data.styles?.format == "alert")
{ {
boxStyle = boxStyle =
{ {
@ -65,17 +50,112 @@ export default function TextBlock({widgetMetaData, data}: StandardBlockComponent
borderRadius: "0.5rem", borderRadius: "0.5rem",
}; };
} }
else if (data.styles?.format == "banner")
{
boxStyle =
{
background: `${color}40`,
padding: "0.5rem",
};
}
let fontSize = "1rem";
if (data.styles?.size)
{
switch (data.styles.size.toLowerCase())
{
case "largest":
fontSize = "3rem";
break;
case "headline":
fontSize = "2rem";
break;
case "title":
fontSize = "1.5rem";
break;
case "body":
fontSize = "1rem";
break;
case "smallest":
fontSize = "0.75rem";
break;
default:
{
if (data.styles.size.match(/^\d+$/))
{
fontSize = `${data.styles.size}px`;
}
else
{
fontSize = "1rem";
}
}
}
}
let fontWeight = "400";
if (data.styles?.weight)
{
switch (data.styles.weight.toLowerCase())
{
case "thin":
case "100":
fontWeight = "100";
break;
case "extralight":
case "200":
fontWeight = "200";
break;
case "light":
case "300":
fontWeight = "300";
break;
case "normal":
case "400":
fontWeight = "400";
break;
case "medium":
case "500":
fontWeight = "500";
break;
case "semibold":
case "600":
fontWeight = "600";
break;
case "bold":
case "700":
fontWeight = "700";
break;
case "extrabold":
case "800":
fontWeight = "800";
break;
case "black":
case "900":
fontWeight = "900";
break;
}
}
const text = data.values.interpolatedText ?? data.values.text; const text = data.values.interpolatedText ?? data.values.text;
const lines = text.split("\n"); const lines = text.split("\n");
const startIcon = data.values.startIcon?.name ? <Icon>{data.values.startIcon.name}</Icon> : null;
const endIcon = data.values.endIcon?.name ? <Icon>{data.values.endIcon.name}</Icon> : null;
return ( return (
<BlockElementWrapper metaData={widgetMetaData} data={data} slot=""> <BlockElementWrapper metaData={widgetMetaData} data={data} slot="">
<Box display="inline-block" lineHeight="1.2" sx={boxStyle}> <Box display="inline-block" lineHeight="1.2" sx={boxStyle}>
<span style={{fontSize: "1rem", color: color}}> <span style={{fontSize: fontSize, color: color, fontWeight: fontWeight}}>
{lines.map((line: string, index: number) => {lines.map((line: string, index: number) =>
( (
<div key={index}>{line}</div> <div key={index}>
<>
{index == 0 && startIcon ? {startIcon} : null}
{line}
{index == lines.length - 1 && endIcon ? {endIcon} : null}
</>
</div>
)) ))
}</span> }</span>
</Box> </Box>

View File

@ -50,6 +50,7 @@ import DeveloperModeUtils from "qqq/utils/DeveloperModeUtils";
import Client from "qqq/utils/qqq/Client"; import Client from "qqq/utils/qqq/Client";
import ValueUtils from "qqq/utils/qqq/ValueUtils"; import ValueUtils from "qqq/utils/qqq/ValueUtils";
import "ace-builds/src-noconflict/ace";
import "ace-builds/src-noconflict/mode-java"; import "ace-builds/src-noconflict/mode-java";
import "ace-builds/src-noconflict/mode-javascript"; import "ace-builds/src-noconflict/mode-javascript";
import "ace-builds/src-noconflict/mode-json"; import "ace-builds/src-noconflict/mode-json";

View File

@ -19,13 +19,16 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import Box from "@mui/material/Box";
import Divider from "@mui/material/Divider"; import Divider from "@mui/material/Divider";
function DividerWidget(): JSX.Element function DividerWidget(): JSX.Element
{ {
return ( return (
<Divider sx={{padding: "1px", background: "red"}}/> <Box pl={3} pt={3} pb={3} width="100%">
<Divider sx={{width: "100%", height: "1px", background: "grey"}} />
</Box>
); );
} }

View File

@ -39,15 +39,15 @@ import {Link, useNavigate} from "react-router-dom";
export interface ChildRecordListData extends WidgetData export interface ChildRecordListData extends WidgetData
{ {
title: string; title?: string;
queryOutput: { records: { values: any }[] }; queryOutput?: { records: { values: any }[] };
childTableMetaData: QTableMetaData; childTableMetaData?: QTableMetaData;
tablePath: string; tablePath?: string;
viewAllLink: string; viewAllLink?: string;
totalRows: number; totalRows?: number;
canAddChildRecord: boolean; canAddChildRecord?: boolean;
defaultValuesForNewChildRecords: { [fieldName: string]: any }; defaultValuesForNewChildRecords?: { [fieldName: string]: any };
disabledFieldsForNewChildRecords: { [fieldName: string]: any }; disabledFieldsForNewChildRecords?: { [fieldName: string]: any };
} }
interface Props interface Props
@ -176,7 +176,7 @@ function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRo
setCsv(csv); setCsv(csv);
setFileName(fileName); setFileName(fileName);
} }
}, [data]); }, [JSON.stringify(data?.queryOutput)]);
/////////////////// ///////////////////
// view all link // // view all link //
@ -295,6 +295,12 @@ function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRo
return (<GridToolbarContainer />); return (<GridToolbarContainer />);
} }
let containerPadding = -3;
if (data?.isInProcess)
{
containerPadding = 0;
}
return ( return (
<Widget <Widget
@ -304,7 +310,7 @@ function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRo
labelAdditionalComponentsRight={labelAdditionalComponentsRight} labelAdditionalComponentsRight={labelAdditionalComponentsRight}
labelBoxAdditionalSx={{position: "relative", top: "-0.375rem"}} labelBoxAdditionalSx={{position: "relative", top: "-0.375rem"}}
> >
<Box mx={-3} mb={-3}> <Box mx={containerPadding} mb={containerPadding}>
<Box> <Box>
<DataGridPro <DataGridPro
autoHeight autoHeight

View File

@ -0,0 +1,96 @@
/*
* 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/>.
*/
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import Modal from "@mui/material/Modal";
import EntityForm from "qqq/components/forms/EntityForm";
import Client from "qqq/utils/qqq/Client";
import React, {useEffect, useReducer, useState} from "react";
////////////////////////////////
// structure of expected data //
////////////////////////////////
export interface ModalEditFormData
{
tableName: string;
defaultValues?: { [key: string]: string };
disabledFields?: { [key: string]: boolean } | string[];
overrideHeading?: string;
onSubmitCallback?: (values: any) => void;
initialShowModalValue?: boolean;
}
const qController = Client.getInstance();
function ModalEditForm({tableName, defaultValues, disabledFields, overrideHeading, onSubmitCallback, initialShowModalValue}: ModalEditFormData,): JSX.Element
{
const [showModal, setShowModal] = useState(initialShowModalValue);
const [table, setTable] = useState(null as QTableMetaData);
const [, forceUpdate] = useReducer((x) => x + 1, 0);
useEffect(() =>
{
if (!tableName)
{
return;
}
(async () =>
{
const tableMetaData = await qController.loadTableMetaData(tableName);
setTable(tableMetaData);
forceUpdate();
})();
}, [tableName]);
/*******************************************************************************
**
*******************************************************************************/
const closeEditChildForm = (event: object, reason: string) =>
{
if (reason === "backdropClick" || reason === "escapeKeyDown")
{
return;
}
setShowModal(null);
};
return (
table && showModal &&
<Modal open={showModal as boolean} onClose={(event, reason) => closeEditChildForm(event, reason)}>
<div className="modalEditForm">
<EntityForm
isModal={true}
closeModalHandler={closeEditChildForm}
table={table}
defaultValues={defaultValues}
disabledFields={disabledFields}
onSubmitCallback={onSubmitCallback}
overrideHeading={overrideHeading}
/>
</div>
</Modal>
);
}
export default ModalEditForm;

View File

@ -66,6 +66,7 @@ import ValidationReview from "qqq/components/processes/ValidationReview";
import {BlockData} from "qqq/components/widgets/blocks/BlockModels"; import {BlockData} from "qqq/components/widgets/blocks/BlockModels";
import CompositeWidget, {CompositeData} from "qqq/components/widgets/CompositeWidget"; import CompositeWidget, {CompositeData} from "qqq/components/widgets/CompositeWidget";
import DashboardWidgets from "qqq/components/widgets/DashboardWidgets"; import DashboardWidgets from "qqq/components/widgets/DashboardWidgets";
import {ChildRecordListData} from "qqq/components/widgets/misc/RecordGridWidget";
import BaseLayout from "qqq/layouts/BaseLayout"; import BaseLayout from "qqq/layouts/BaseLayout";
import ProcessWidgetBlockUtils from "qqq/pages/processes/ProcessWidgetBlockUtils"; import ProcessWidgetBlockUtils from "qqq/pages/processes/ProcessWidgetBlockUtils";
import {TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT} from "qqq/pages/records/query/RecordQuery"; import {TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT} from "qqq/pages/records/query/RecordQuery";
@ -95,10 +96,10 @@ const INITIAL_RETRY_MILLIS = 1_500;
const RETRY_MAX_MILLIS = 12_000; const RETRY_MAX_MILLIS = 12_000;
const BACKOFF_AMOUNT = 1.5; const BACKOFF_AMOUNT = 1.5;
/////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
// define some functions that we can make referene to, which we'll overwrite // // define some functions that we can make reference to, which we'll overwrite //
// with functions from formik, once we're inside formik. // // with functions from formik, once we're inside formik. //
/////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
let formikSetFieldValueFunction = (field: string, value: any, shouldValidate?: boolean): void => let formikSetFieldValueFunction = (field: string, value: any, shouldValidate?: boolean): void =>
{ {
}; };
@ -150,6 +151,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
const [previouslySeenUpdatedFieldMetaDataMap, setPreviouslySeenUpdatedFieldMetaDataMap] = useState(new Map<string, QFieldMetaData>); const [previouslySeenUpdatedFieldMetaDataMap, setPreviouslySeenUpdatedFieldMetaDataMap] = useState(new Map<string, QFieldMetaData>);
const [renderedWidgets, setRenderedWidgets] = useState({} as { [step: string]: { [widgetName: string]: any } }); const [renderedWidgets, setRenderedWidgets] = useState({} as { [step: string]: { [widgetName: string]: any } });
const [controlCallbacks, setControlCallbacks] = useState({} as { [name: string]: () => void });
const {pageHeader, recordAnalytics, setPageHeader, helpHelpActive} = useContext(QContext); const {pageHeader, recordAnalytics, setPageHeader, helpHelpActive} = useContext(QContext);
@ -209,7 +211,8 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
const [recordConfig, setRecordConfig] = useState({} as any); const [recordConfig, setRecordConfig] = useState({} as any);
const [pageNumber, setPageNumber] = useState(0); const [pageNumber, setPageNumber] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(10); const [rowsPerPage, setRowsPerPage] = useState(10);
const [records, setRecords] = useState([] as QRecord[]); const [records, setRecords] = useState([] as any);
const [childRecordData, setChildRecordData] = useState(null as ChildRecordListData);
////////////////////////////// //////////////////////////////
// state for bulk edit form // // state for bulk edit form //
@ -328,37 +331,114 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
*******************************************************************************/ *******************************************************************************/
function renderWidget(widgetName: string) function renderWidget(widgetName: string)
{ {
const widgetMetaData = qInstance.widgets.get(widgetName);
if (!widgetMetaData)
{
return (<Alert color="error">Unrecognized widget name: {widgetName}</Alert>);
}
if (!renderedWidgets[activeStep.name]) if (!renderedWidgets[activeStep.name])
{ {
renderedWidgets[activeStep.name] = {}; renderedWidgets[activeStep.name] = {};
setRenderedWidgets(renderedWidgets); setRenderedWidgets(renderedWidgets);
} }
if (renderedWidgets[activeStep.name][widgetName]) let isChildRecordWidget = widgetMetaData.type == "childRecordList";
if (!isChildRecordWidget && renderedWidgets[activeStep.name][widgetName])
{ {
return renderedWidgets[activeStep.name][widgetName]; return renderedWidgets[activeStep.name][widgetName];
} }
const widgetMetaData = qInstance.widgets.get(widgetName);
if (!widgetMetaData)
{
return (<Alert color="error">Unrecognized widget name: {widgetName}</Alert>);
}
const queryStringParts: string[] = []; const queryStringParts: string[] = [];
for (let name in processValues) for (let name in processValues)
{ {
queryStringParts.push(`${name}=${encodeURIComponent(processValues[name])}`); queryStringParts.push(`${name}=${encodeURIComponent(processValues[name])}`);
} }
let initialWidgetDataList = null;
if (processValues[widgetName])
{
processValues[widgetName].hasPermission = true;
initialWidgetDataList = [processValues[widgetName]];
}
let actionCallback = blockWidgetActionCallback;
if (isChildRecordWidget)
{
actionCallback = childRecordListWidgetActionCallBack;
if (childRecordData)
{
initialWidgetDataList = [childRecordData];
}
}
const renderedWidget = (<Box m={-2}> const renderedWidget = (<Box m={-2}>
<DashboardWidgets widgetMetaDataList={[widgetMetaData]} omitWrappingGridContainer={true} childUrlParams={queryStringParts.join("&")} /> <DashboardWidgets widgetMetaDataList={[widgetMetaData]} omitWrappingGridContainer={true} childUrlParams={queryStringParts.join("&")} initialWidgetDataList={initialWidgetDataList} values={processValues} actionCallback={actionCallback} />
</Box>); </Box>);
renderedWidgets[activeStep.name][widgetName] = renderedWidget; renderedWidgets[activeStep.name][widgetName] = renderedWidget;
return renderedWidget; return renderedWidget;
} }
/***************************************************************************
**
***************************************************************************/
function handleControlCode(controlCode: string)
{
const split = controlCode.split(":", 2);
let controlCallbackName: string;
let controlCallbackValue: any;
if (split.length == 2)
{
if (split[0] == "showModal")
{
processValues[split[1]] = true;
controlCallbackName = split[1];
controlCallbackValue = true;
}
else if (split[0] == "hideModal")
{
processValues[split[1]] = false;
controlCallbackName = split[1];
controlCallbackValue = false;
}
else if (split[0] == "toggleModal")
{
const currentValue = processValues[split[1]];
processValues[split[1]] = !!!currentValue;
controlCallbackName = split[1];
controlCallbackValue = processValues[split[1]];
}
else
{
console.log(`Unexpected part[0] (before colon) in controlCode: [${controlCode}]`);
}
}
else
{
console.log(`Expected controlCode to have 2 colon-delimited parts, but was: [${controlCode}]`);
}
if (controlCallbackName && controlCallbacks[controlCallbackName])
{
// @ts-ignore ... args are hard
controlCallbacks[controlCallbackName](controlCallbackValue);
}
}
/***************************************************************************
** callback used by child list widget
***************************************************************************/
function childRecordListWidgetActionCallBack(data: any): boolean
{
console.log(`in childRecordListWidgetActionCallBack: ${JSON.stringify(data)}`);
setChildRecordData(data as ChildRecordListData);
return (true);
}
/*************************************************************************** /***************************************************************************
** callback used by widget blocks, e.g., for input-text-enter-on-submit, ** callback used by widget blocks, e.g., for input-text-enter-on-submit,
** and action buttons. ** and action buttons.
@ -367,30 +447,59 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
{ {
console.log(`in blockWidgetActionCallback, called by block: ${JSON.stringify(blockData)}`); console.log(`in blockWidgetActionCallback, called by block: ${JSON.stringify(blockData)}`);
/////////////////////////////////////////////////////////////////////////////// if (eventValues?.registerControlCallbackName && eventValues?.registerControlCallbackFunction)
// if the eventValues included an actionCode - validate it before proceeding //
///////////////////////////////////////////////////////////////////////////////
if (eventValues && eventValues.actionCode && !ProcessWidgetBlockUtils.isActionCodeValid(eventValues.actionCode, activeStep, processValues))
{ {
setFormError("Unrecognized action code: " + eventValues.actionCode); controlCallbacks[eventValues.registerControlCallbackName] = eventValues.registerControlCallbackFunction;
setControlCallbacks(controlCallbacks);
if (eventValues["_fieldToClearIfError"]) return (true);
{
/////////////////////////////////////////////////////////////////////////////
// if the eventValues included a _fieldToClearIfError, well, then do that. //
/////////////////////////////////////////////////////////////////////////////
formikSetFieldValueFunction(eventValues["_fieldToClearIfError"], "", false);
} }
return (false); ////////////////////////////////////////////////////////////////////////////////////////////////////////
// we don't validate these on the android frontend, and it seems fine - just let the app validate it? //
////////////////////////////////////////////////////////////////////////////////////////////////////////
// ///////////////////////////////////////////////////////////////////////////////
// // if the eventValues included an actionCode - validate it before proceeding //
// ///////////////////////////////////////////////////////////////////////////////
// if (eventValues && eventValues.actionCode && !ProcessWidgetBlockUtils.isActionCodeValid(eventValues.actionCode, activeStep, processValues))
// {
// setFormError("Unrecognized action code: " + eventValues.actionCode);
// if (eventValues["_fieldToClearIfError"])
// {
// /////////////////////////////////////////////////////////////////////////////
// // if the eventValues included a _fieldToClearIfError, well, then do that. //
// /////////////////////////////////////////////////////////////////////////////
// formikSetFieldValueFunction(eventValues["_fieldToClearIfError"], "", false);
// }
// return (false);
// }
let doSubmit = false;
if (blockData?.blockTypeName == "BUTTON" && eventValues?.actionCode)
{
doSubmit = true;
}
else if (blockData?.blockTypeName == "BUTTON" && eventValues?.controlCode)
{
handleControlCode(eventValues.controlCode);
doSubmit = false;
}
else if (blockData?.blockTypeName == "INPUT_FIELD")
{
///////////////////////////////////////////////////////////////////////////////////////////////
// if action callback was fired from an input field, assume that means we're good to submit. //
///////////////////////////////////////////////////////////////////////////////////////////////
doSubmit = true;
} }
////////////////// //////////////////
// ok - submit! // // ok - submit! //
////////////////// //////////////////
handleSubmit(eventValues); if (doSubmit)
{
handleFormSubmit(eventValues);
return (true); return (true);
} }
}
/*************************************************************************** /***************************************************************************
@ -412,7 +521,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
ProcessWidgetBlockUtils.dynamicEvaluationOfCompositeWidgetData(compositeWidgetData, processValues); ProcessWidgetBlockUtils.dynamicEvaluationOfCompositeWidgetData(compositeWidgetData, processValues);
renderedWidgets[key] = <Box key={key} pt={2}> renderedWidgets[key] = <Box key={key} pt={2}>
<CompositeWidget widgetMetaData={widgetMetaData} data={compositeWidgetData} actionCallback={blockWidgetActionCallback} /> <CompositeWidget widgetMetaData={widgetMetaData} data={compositeWidgetData} actionCallback={blockWidgetActionCallback} values={processValues} />
</Box>; </Box>;
setRenderedWidgets(renderedWidgets); setRenderedWidgets(renderedWidgets);
@ -574,6 +683,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
let helpRoles = ["PROCESS_SCREEN", "ALL_SCREENS"]; let helpRoles = ["PROCESS_SCREEN", "ALL_SCREENS"];
const showHelp = helpHelpActive || hasHelpContent(step.helpContents, helpRoles); const showHelp = helpHelpActive || hasHelpContent(step.helpContents, helpRoles);
const formattedHelpContent = <HelpContent helpContents={step.helpContents} roles={helpRoles} helpContentKey={`process:${processName};step:${step?.name}`} />; const formattedHelpContent = <HelpContent helpContents={step.helpContents} roles={helpRoles} helpContentKey={`process:${processName};step:${step?.name}`} />;
const isFormatScanner = step?.format?.toLowerCase() == "scanner";
return ( return (
<> <>
@ -582,7 +692,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
// hide label on widgets - the Widget component itself provides the label // // hide label on widgets - the Widget component itself provides the label //
// for modals, show the process label, but not for full-screen processes (for them, it is in the breadcrumb) // // for modals, show the process label, but not for full-screen processes (for them, it is in the breadcrumb) //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
!isWidget && !isWidget && !isFormatScanner &&
<MDTypography variant={isWidget ? "h6" : "h5"} component="div" fontWeight="bold"> <MDTypography variant={isWidget ? "h6" : "h5"} component="div" fontWeight="bold">
{(isModal) ? `${overrideLabel ?? process.label}: ` : ""} {(isModal) ? `${overrideLabel ?? process.label}: ` : ""}
{step?.label} {step?.label}
@ -1044,11 +1154,11 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
if (doesStepHaveComponent(activeStep, QComponentType.WIDGET)) if (doesStepHaveComponent(activeStep, QComponentType.WIDGET))
{ {
ProcessWidgetBlockUtils.addFieldsForCompositeWidget(activeStep, (fieldMetaData) => ProcessWidgetBlockUtils.addFieldsForCompositeWidget(activeStep, processValues, (fieldMetaData) =>
{ {
const dynamicField = DynamicFormUtils.getDynamicField(fieldMetaData); const dynamicField = DynamicFormUtils.getDynamicField(fieldMetaData);
const validation = DynamicFormUtils.getValidationForField(fieldMetaData); const validation = DynamicFormUtils.getValidationForField(fieldMetaData);
addField(fieldMetaData.name, dynamicField, processValues[fieldMetaData.name], validation) addField(fieldMetaData.name, dynamicField, processValues[fieldMetaData.name], validation);
}); });
} }
@ -1210,6 +1320,11 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
const {records} = response; const {records} = response;
setRecords(records); setRecords(records);
if (!childRecordData || childRecordData.length == 0)
{
setChildRecordData(convertRecordsToChildRecordData(records));
}
///////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////
// re-construct the recordConfig object, so the setState call triggers a new rendering // // re-construct the recordConfig object, so the setState call triggers a new rendering //
///////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////
@ -1232,6 +1347,30 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
}, [needRecords]); }, [needRecords]);
/***************************************************************************
**
***************************************************************************/
function convertRecordsToChildRecordData(records: QRecord[])
{
const frontendRecords = [] as any[];
records.forEach((record: QRecord) =>
{
const object = {
"tableName": record.tableName,
"recordLabel": record.recordLabel,
"errors": record.errors,
"warnings": record.warnings,
"values": Object.fromEntries(record.values),
"displayValues": Object.fromEntries(record.displayValues),
};
frontendRecords.push(object);
});
const newChildListData = {} as ChildRecordListData;
newChildListData.queryOutput = {records: frontendRecords};
return (newChildListData);
}
/*************************************************************************** /***************************************************************************
** **
***************************************************************************/ ***************************************************************************/
@ -1605,11 +1744,35 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
setNewStep(activeStepIndex - 1); setNewStep(activeStepIndex - 1);
}; };
////////////////////////////////////////////
// handle user submitting changed records //
////////////////////////////////////////////
const doSubmit = async (formData: FormData) =>
{
const formDataHeaders = {
"content-type": "multipart/form-data; boundary=--------------------------320289315924586491558366",
};
setTimeout(async () =>
{
recordAnalytics({category: "processEvents", action: "processStep", label: activeStep.label});
const processResponse = await Client.getInstance().processStep(
processName,
processUUID,
activeStep.name,
formData,
formDataHeaders
);
setLastProcessResponse(processResponse);
});
};
////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////
// handle user submitting the form - which in qqq means moving forward from any screen. // // handle user submitting the form - which in qqq means moving forward from any screen. //
// caller can pass in a map of values to be added to the form data too // // caller can pass in a map of values to be added to the form data too //
////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////
const handleSubmit = async (values: any) => const handleFormSubmit = async (values: any) =>
{ {
setFormError(null); setFormError(null);
@ -1648,28 +1811,20 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
formData.append("bulkEditEnabledFields", bulkEditEnabledFields.join(",")); formData.append("bulkEditEnabledFields", bulkEditEnabledFields.join(","));
} }
const formDataHeaders = { /////////////////////////////////////////////////////////////
"content-type": "multipart/form-data; boundary=--------------------------320289315924586491558366", // convert to regular objects so that they can be jsonized //
}; /////////////////////////////////////////////////////////////
if (childRecordData)
{
formData.append("frontendRecords", JSON.stringify(childRecordData.queryOutput.records));
}
setProcessValues({}); setProcessValues({});
setRecords([]); setRecords([]);
setOverrideOnLastStep(null); setOverrideOnLastStep(null);
setLastProcessResponse(new QJobRunning({message: "Working..."})); setLastProcessResponse(new QJobRunning({message: "Working..."}));
setTimeout(async () => doSubmit(formData);
{
recordAnalytics({category: "processEvents", action: "processStep", label: activeStep.label});
const processResponse = await Client.getInstance().processStep(
processName,
processUUID,
activeStep.name,
formData,
formDataHeaders,
);
setLastProcessResponse(processResponse);
});
}; };
@ -1744,7 +1899,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
mainCardStyles.display = "flex"; mainCardStyles.display = "flex";
} }
return mainCardStyles return mainCardStyles;
} }
let nextButtonLabel = "Next"; let nextButtonLabel = "Next";
@ -1769,7 +1924,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
initialValues={initialValues} initialValues={initialValues}
validationSchema={validationScheme} validationSchema={validationScheme}
validation={validationFunction} validation={validationFunction}
onSubmit={handleSubmit} onSubmit={handleFormSubmit}
> >
{({ {({
values, errors, touched, isSubmitting, setFieldValue, setTouched values, errors, touched, isSubmitting, setFieldValue, setTouched
@ -1827,7 +1982,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
) : ( ) : (
<MDButton variant="gradient" color="light" onClick={handleBack}>back</MDButton> <MDButton variant="gradient" color="light" onClick={handleBack}>back</MDButton>
)} )}
{processError || qJobRunning || !activeStep ? ( {processError || qJobRunning || !activeStep || activeStep?.format?.toLowerCase() == "scanner" ? (
<Box /> <Box />
) : ( ) : (
<> <>

View File

@ -71,11 +71,11 @@ export default class ProcessWidgetBlockUtils
} }
// else, continue... // else, continue...
} }
else if (block.blockTypeName == "ACTION_BUTTON") else if (block.blockTypeName == "BUTTON")
{ {
////////////////////////////////////////////////////////// //////////////////////////////////////////
// actually look at actionCodes on action button blocks // // look at actionCodes on button blocks //
////////////////////////////////////////////////////////// //////////////////////////////////////////
if (block.values?.actionCode == actionCode) if (block.values?.actionCode == actionCode)
{ {
return (true); return (true);
@ -182,7 +182,7 @@ export default class ProcessWidgetBlockUtils
/*************************************************************************** /***************************************************************************
** **
***************************************************************************/ ***************************************************************************/
public static addFieldsForCompositeWidget(step: QFrontendStepMetaData, addFieldCallback: (fieldMetaData: QFieldMetaData) => void) public static addFieldsForCompositeWidget(step: QFrontendStepMetaData, processValues: any, addFieldCallback: (fieldMetaData: QFieldMetaData) => void)
{ {
/////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////
// private recursive function to walk the composite tree // // private recursive function to walk the composite tree //
@ -200,7 +200,7 @@ export default class ProcessWidgetBlockUtils
else if (block.blockTypeName == "INPUT_FIELD") else if (block.blockTypeName == "INPUT_FIELD")
{ {
const fieldMetaData = new QFieldMetaData(block.values?.fieldMetaData); const fieldMetaData = new QFieldMetaData(block.values?.fieldMetaData);
addFieldCallback(fieldMetaData) addFieldCallback(fieldMetaData);
} }
} }
} }
@ -210,14 +210,57 @@ export default class ProcessWidgetBlockUtils
} }
} }
///////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// foreach component, if it's an adhoc widget, call recursive helper on it // // foreach component, if it's an adhoc widget or a widget w/ its data in the processValues, then, call recursive helper on it //
///////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
for (let component of step.components) for (let component of step.components)
{ {
if (component.type == QComponentType.WIDGET && component.values?.isAdHocWidget) if (component.type == QComponentType.WIDGET && component.values?.isAdHocWidget)
{ {
recursiveHelper(component.values as unknown as CompositeData) recursiveHelper(component.values as unknown as CompositeData);
}
else if (component.type == QComponentType.WIDGET && processValues[component.values?.widgetName])
{
recursiveHelper(processValues[component.values?.widgetName] as unknown as CompositeData);
}
}
}
/***************************************************************************
**
***************************************************************************/
public static processColorFromStyleMap(colorFromStyleMap?: string): string
{
if (colorFromStyleMap)
{
switch (colorFromStyleMap.toUpperCase())
{
case "SUCCESS":
return("#2BA83F");
case "WARNING":
return("#FBA132");
case "ERROR":
return("#FB4141");
case "INFO":
return("#458CFF");
case "MUTED":
return("#7b809a");
default:
{
if (colorFromStyleMap.match(/^[0-9A-F]{6}$/))
{
return(`#${colorFromStyleMap}`);
}
else if (colorFromStyleMap.match(/^[0-9A-F]{8}$/))
{
return(`#${colorFromStyleMap}`);
}
else
{
return(colorFromStyleMap);
}
}
} }
} }
} }

View File

@ -34,6 +34,7 @@ import BaseLayout from "qqq/layouts/BaseLayout";
import Client from "qqq/utils/qqq/Client"; import Client from "qqq/utils/qqq/Client";
import ValueUtils from "qqq/utils/qqq/ValueUtils"; import ValueUtils from "qqq/utils/qqq/ValueUtils";
import "ace-builds/src-noconflict/ace";
import "ace-builds/src-noconflict/mode-java"; import "ace-builds/src-noconflict/mode-java";
import "ace-builds/src-noconflict/mode-javascript"; import "ace-builds/src-noconflict/mode-javascript";
import "ace-builds/src-noconflict/mode-json"; import "ace-builds/src-noconflict/mode-json";

View File

@ -205,6 +205,8 @@ function RecordView({table, record: overrideRecord, launchProcess}: Props): JSX.
const {accentColor, setPageHeader, tableMetaData, setTableMetaData, tableProcesses, setTableProcesses, dotMenuOpen, keyboardHelpOpen, helpHelpActive, recordAnalytics, userId: currentUserId} = useContext(QContext); const {accentColor, setPageHeader, tableMetaData, setTableMetaData, tableProcesses, setTableProcesses, dotMenuOpen, keyboardHelpOpen, helpHelpActive, recordAnalytics, userId: currentUserId} = useContext(QContext);
const CREATE_CHILD_KEY = "createChild";
if (localStorage.getItem(tableVariantLocalStorageKey)) if (localStorage.getItem(tableVariantLocalStorageKey))
{ {
tableVariant = JSON.parse(localStorage.getItem(tableVariantLocalStorageKey)); tableVariant = JSON.parse(localStorage.getItem(tableVariantLocalStorageKey));
@ -307,12 +309,19 @@ function RecordView({table, record: overrideRecord, launchProcess}: Props): JSX.
/////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////
// the path for a process looks like: .../table/id/process // // the path for a process looks like: .../table/id/process //
// the path for creating a child record looks like: .../table/id/createChild/:childTableName // // the path for creating a child record looks like: .../table/id/createChild/:childTableName //
// the path for creating a child record in a process looks like: //
// .../table/id/processName#/createChild=... //
/////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////
let hasChildRecordKey = pathParts.some(p => p.includes(CREATE_CHILD_KEY));
if (!hasChildRecordKey)
{
hasChildRecordKey = hashParts.some(h => h.includes(CREATE_CHILD_KEY));
}
////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if our tableName is in the -3 index, try to open process // // if our tableName is in the -3 index, and there is no token for updating child records, try to open process //
////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if (pathParts[pathParts.length - 3] === tableName) if (!hasChildRecordKey && pathParts[pathParts.length - 3] === tableName)
{ {
const processName = pathParts[pathParts.length - 1]; const processName = pathParts[pathParts.length - 1];
const processList = allTableProcesses.filter(p => p.name.endsWith(processName)); const processList = allTableProcesses.filter(p => p.name.endsWith(processName));
@ -349,7 +358,7 @@ function RecordView({table, record: overrideRecord, launchProcess}: Props): JSX.
// if our table is in the -4 index, and there's `createChild` in the -2 index, try to open a createChild form // // if our table is in the -4 index, and there's `createChild` in the -2 index, try to open a createChild form //
// e.g., person/42/createChild/address (to create an address under person 42) // // e.g., person/42/createChild/address (to create an address under person 42) //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if (pathParts[pathParts.length - 4] === tableName && pathParts[pathParts.length - 2] == "createChild") if (pathParts[pathParts.length - 4] === tableName && pathParts[pathParts.length - 2] == CREATE_CHILD_KEY)
{ {
(async () => (async () =>
{ {
@ -368,7 +377,7 @@ function RecordView({table, record: overrideRecord, launchProcess}: Props): JSX.
for (let i = 0; i < hashParts.length; i++) for (let i = 0; i < hashParts.length; i++)
{ {
const parts = hashParts[i].split("="); const parts = hashParts[i].split("=");
if (parts.length > 1 && parts[0] == "createChild") if (parts.length > 1 && parts[0] == CREATE_CHILD_KEY)
{ {
(async () => (async () =>
{ {
@ -831,7 +840,7 @@ function RecordView({table, record: overrideRecord, launchProcess}: Props): JSX.
const ownerId = record.values.get(tableMetaData.shareableTableMetaData.thisTableOwnerIdFieldName); const ownerId = record.values.get(tableMetaData.shareableTableMetaData.thisTableOwnerIdFieldName);
if (ownerId != currentUserId) if (ownerId != currentUserId)
{ {
disabledTooltipText = `Only the owner of a ${tableMetaData.label} may share it.` disabledTooltipText = `Only the owner of a ${tableMetaData.label} may share it.`;
shareDisabled = true; shareDisabled = true;
} }
else else

View File

@ -113,7 +113,7 @@ export default class DataGridUtils
{ {
console.log(`row-click mouse-up happened ${diff} x or y pixels away from the mouse-down - so not considering it a click.`); console.log(`row-click mouse-up happened ${diff} x or y pixels away from the mouse-down - so not considering it a click.`);
} }
} };
/******************************************************************************* /*******************************************************************************
** **
@ -170,7 +170,7 @@ export default class DataGridUtils
}); });
return (rows); return (rows);
} };
/******************************************************************************* /*******************************************************************************
** **
@ -241,16 +241,20 @@ export default class DataGridUtils
/////////////////////////// ///////////////////////////
// sort by labels... mmm // // sort by labels... mmm //
/////////////////////////// ///////////////////////////
sortedKeys.push(...tableMetaData.fields.keys()) sortedKeys.push(...tableMetaData.fields.keys());
sortedKeys.sort((a: string, b: string): number => sortedKeys.sort((a: string, b: string): number =>
{ {
return (tableMetaData.fields.get(a).label.localeCompare(tableMetaData.fields.get(b).label)) return (tableMetaData.fields.get(a).label.localeCompare(tableMetaData.fields.get(b).label));
}) });
} }
sortedKeys.forEach((key) => sortedKeys.forEach((key) =>
{ {
const field = tableMetaData.fields.get(key); const field = tableMetaData.fields.get(key);
if (!field)
{
return;
}
if (field.isHeavy) if (field.isHeavy)
{ {
if (field.type == QFieldType.BLOB) if (field.type == QFieldType.BLOB)
@ -346,7 +350,7 @@ export default class DataGridUtils
(cellValues.value) (cellValues.value)
); );
const helpRoles = ["QUERY_SCREEN", "READ_SCREENS", "ALL_SCREENS"] const helpRoles = ["QUERY_SCREEN", "READ_SCREENS", "ALL_SCREENS"];
const showHelp = hasHelpContent(field.helpContents, helpRoles); // todo - maybe - take helpHelpActive from context all the way down to here? const showHelp = hasHelpContent(field.helpContents, helpRoles); // todo - maybe - take helpHelpActive from context all the way down to here?
if (showHelp) if (showHelp)
{ {
@ -361,7 +365,7 @@ export default class DataGridUtils
} }
return (column); return (column);
} };
/******************************************************************************* /*******************************************************************************
@ -415,6 +419,6 @@ export default class DataGridUtils
} }
return (200); return (200);
} };
} }

View File

@ -19,7 +19,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import Client from "qqq/utils/qqq/Client"; import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType";
/******************************************************************************* /*******************************************************************************
** Utility functions for basic html/webpage/browser things. ** Utility functions for basic html/webpage/browser things.
@ -68,10 +69,16 @@ export default class HtmlUtils
** it was originally built like this when we had to submit full access token to backend... ** it was originally built like this when we had to submit full access token to backend...
** **
*******************************************************************************/ *******************************************************************************/
static downloadUrlViaIFrame = (url: string, filename: string) => static downloadUrlViaIFrame = (field: QFieldMetaData, url: string, filename: string) =>
{ {
if(url.startsWith("data:")) if (url.startsWith("data:") || url.startsWith("http"))
{ {
if (url.startsWith("http"))
{
const separator = url.includes("?") ? "&" : "?";
url += encodeURIComponent(`${separator}response-content-disposition=attachment; ${filename}`);
}
const link = document.createElement("a"); const link = document.createElement("a");
link.download = filename; link.download = filename;
link.href = url; link.href = url;
@ -93,8 +100,14 @@ export default class HtmlUtils
// todo - onload event handler to let us know when done? // todo - onload event handler to let us know when done?
document.body.appendChild(iframe); document.body.appendChild(iframe);
var method = "get";
if (QFieldType.BLOB == field.type)
{
method = "post";
}
const form = document.createElement("form"); const form = document.createElement("form");
form.setAttribute("method", "post"); form.setAttribute("method", method);
form.setAttribute("action", url); form.setAttribute("action", url);
form.setAttribute("target", "downloadIframe"); form.setAttribute("target", "downloadIframe");
iframe.appendChild(form); iframe.appendChild(form);

View File

@ -133,7 +133,7 @@ class FilterUtils
} }
else else
{ {
values = await qController.possibleValues(fieldTable.name, null, field.name, "", values); values = await qController.possibleValues(fieldTable.name, null, field.name, "", values, undefined, "filter");
} }
} }

View File

@ -28,18 +28,17 @@ import "datejs"; // https://github.com/datejs/Datejs
import {Chip, ClickAwayListener, Icon} from "@mui/material"; import {Chip, ClickAwayListener, Icon} from "@mui/material";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import IconButton from "@mui/material/IconButton";
import Tooltip from "@mui/material/Tooltip"; import Tooltip from "@mui/material/Tooltip";
import {makeStyles} from "@mui/styles";
import parse from "html-react-parser"; 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 HtmlUtils from "qqq/utils/HtmlUtils";
import Client from "qqq/utils/qqq/Client"; import Client from "qqq/utils/qqq/Client";
import "ace-builds/src-noconflict/ace";
import "ace-builds/src-noconflict/mode-sql"; import "ace-builds/src-noconflict/mode-sql";
import React, {Fragment, useReducer, useState} from "react";
import AceEditor from "react-ace";
import "ace-builds/src-noconflict/mode-velocity"; import "ace-builds/src-noconflict/mode-velocity";
import {Link} from "react-router-dom";
/******************************************************************************* /*******************************************************************************
** Utility class for working with QQQ Values ** Utility class for working with QQQ Values
@ -198,7 +197,7 @@ class ValueUtils
); );
} }
if (field.type == QFieldType.BLOB) if (field.type == QFieldType.BLOB || field.hasAdornment(AdornmentType.FILE_DOWNLOAD))
{ {
return (<BlobComponent field={field} url={rawValue} filename={displayValue} usage={usage} />); return (<BlobComponent field={field} url={rawValue} filename={displayValue} usage={usage} />);
} }
@ -276,7 +275,7 @@ class ValueUtils
// to millis) back to it // // to millis) back to it //
//////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////
date = new Date(date); date = new Date(date);
date.setTime(date.getTime() + date.getTimezoneOffset() * 60 * 1000) date.setTime(date.getTime() + date.getTimezoneOffset() * 60 * 1000);
} }
// @ts-ignore // @ts-ignore
return (`${date.toString("yyyy-MM-dd")}`); return (`${date.toString("yyyy-MM-dd")}`);
@ -680,7 +679,7 @@ function BlobComponent({field, url, filename, usage}: BlobComponentProps): JSX.E
const download = (event: React.MouseEvent<HTMLSpanElement>) => const download = (event: React.MouseEvent<HTMLSpanElement>) =>
{ {
event.stopPropagation(); event.stopPropagation();
HtmlUtils.downloadUrlViaIFrame(url, filename); HtmlUtils.downloadUrlViaIFrame(field, url, filename);
}; };
const open = (event: React.MouseEvent<HTMLSpanElement>) => const open = (event: React.MouseEvent<HTMLSpanElement>) =>
@ -704,10 +703,22 @@ function BlobComponent({field, url, filename, usage}: BlobComponentProps): JSX.E
usage == "view" && filename usage == "view" && filename
} }
<Tooltip placement={tooltipPlacement} title="Open file"> <Tooltip placement={tooltipPlacement} title="Open file">
{
field.type == QFieldType.BLOB ? (
<Icon className={"blobIcon"} fontSize="small" onClick={(e) => open(e)}>open_in_new</Icon> <Icon className={"blobIcon"} fontSize="small" onClick={(e) => open(e)}>open_in_new</Icon>
) : (
<a style={{color: "inherit"}} rel="noopener noreferrer" href={url} target="_blank"><Icon className={"blobIcon"} fontSize="small">open_in_new</Icon></a>
)
}
</Tooltip> </Tooltip>
<Tooltip placement={tooltipPlacement} title="Download file"> <Tooltip placement={tooltipPlacement} title="Download file">
{
field.type == QFieldType.BLOB ? (
<Icon className={"blobIcon"} fontSize="small" onClick={(e) => download(e)}>save_alt</Icon> <Icon className={"blobIcon"} fontSize="small" onClick={(e) => download(e)}>save_alt</Icon>
) : (
<a style={{color: "inherit"}} href={url} download="test.pdf"><Icon className={"blobIcon"} fontSize="small">save_alt</Icon></a>
)
}
</Tooltip> </Tooltip>
{ {
usage == "query" && filename usage == "query" && filename
@ -717,5 +728,4 @@ function BlobComponent({field, url, filename, usage}: BlobComponentProps): JSX.E
} }
export default ValueUtils; export default ValueUtils;

View File

@ -57,4 +57,5 @@ module.exports = function (app)
app.use("/images", getRequestHandler()); app.use("/images", getRequestHandler());
app.use("/api*", getRequestHandler()); app.use("/api*", getRequestHandler());
app.use("/*api", getRequestHandler()); app.use("/*api", getRequestHandler());
app.use("/qqq/*", getRequestHandler());
}; };