mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-18 13:20:43 +00:00
Merge pull request #73 from Kingsrook/feature/CE-1727-mobile-first-uiux
Feature/ce 1727 mobile first uiux
This commit is contained in:
@ -30,14 +30,17 @@ import MDButton from "qqq/components/legacy/MDButton";
|
||||
|
||||
export const standardWidth = "150px";
|
||||
|
||||
const standardML = {xs: 1, md: 3};
|
||||
|
||||
interface QCreateNewButtonProps
|
||||
{
|
||||
tablePath: string;
|
||||
}
|
||||
|
||||
export function QCreateNewButton({tablePath}: QCreateNewButtonProps): JSX.Element
|
||||
{
|
||||
return (
|
||||
<Box display="inline-block" ml={3} mr={0} width={standardWidth}>
|
||||
<Box display="inline-block" ml={standardML} mr={0} width={standardWidth}>
|
||||
<Link to={`${tablePath}/create`}>
|
||||
<MDButton variant="gradient" color="info" fullWidth startIcon={<Icon>add</Icon>}>
|
||||
Create New
|
||||
@ -54,6 +57,7 @@ interface QSaveButtonProps
|
||||
onClickHandler?: any,
|
||||
disabled: boolean
|
||||
}
|
||||
|
||||
QSaveButton.defaultProps = {
|
||||
label: "Save",
|
||||
iconName: "save"
|
||||
@ -62,7 +66,7 @@ QSaveButton.defaultProps = {
|
||||
export function QSaveButton({label, iconName, onClickHandler, disabled}: QSaveButtonProps): JSX.Element
|
||||
{
|
||||
return (
|
||||
<Box ml={3} width={standardWidth}>
|
||||
<Box ml={standardML} width={standardWidth}>
|
||||
<MDButton type="submit" variant="gradient" color="info" size="small" onClick={onClickHandler} fullWidth startIcon={<Icon>{iconName}</Icon>} disabled={disabled}>
|
||||
{label}
|
||||
</MDButton>
|
||||
@ -72,17 +76,18 @@ export function QSaveButton({label, iconName, onClickHandler, disabled}: QSaveBu
|
||||
|
||||
interface QDeleteButtonProps
|
||||
{
|
||||
onClickHandler: any
|
||||
disabled?: boolean
|
||||
onClickHandler: any;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
QDeleteButton.defaultProps = {
|
||||
disabled: false
|
||||
};
|
||||
|
||||
export function QDeleteButton({onClickHandler, disabled}: QDeleteButtonProps): JSX.Element
|
||||
{
|
||||
return (
|
||||
<Box ml={3} width={standardWidth}>
|
||||
<Box ml={standardML} width={standardWidth}>
|
||||
<MDButton variant="gradient" color="primary" size="small" onClick={onClickHandler} fullWidth startIcon={<Icon>delete</Icon>} disabled={disabled}>
|
||||
Delete
|
||||
</MDButton>
|
||||
@ -93,7 +98,7 @@ export function QDeleteButton({onClickHandler, disabled}: QDeleteButtonProps): J
|
||||
export function QEditButton(): JSX.Element
|
||||
{
|
||||
return (
|
||||
<Box ml={3} width={standardWidth}>
|
||||
<Box ml={standardML} width={standardWidth}>
|
||||
<Link to="edit">
|
||||
<MDButton variant="gradient" color="dark" size="small" fullWidth startIcon={<Icon>edit</Icon>}>
|
||||
Edit
|
||||
@ -132,7 +137,7 @@ interface QCancelButtonProps
|
||||
onClickHandler: any;
|
||||
disabled: boolean;
|
||||
label?: string;
|
||||
iconName?: string
|
||||
iconName?: string;
|
||||
}
|
||||
|
||||
export function QCancelButton({
|
||||
@ -140,7 +145,7 @@ export function QCancelButton({
|
||||
}: QCancelButtonProps): JSX.Element
|
||||
{
|
||||
return (
|
||||
<Box ml="auto" width={standardWidth}>
|
||||
<Box ml={standardML} mb={2} width={standardWidth}>
|
||||
<MDButton type="button" variant="outlined" color="dark" size="small" fullWidth startIcon={<Icon>{iconName}</Icon>} onClick={onClickHandler} disabled={disabled}>
|
||||
{label}
|
||||
</MDButton>
|
||||
@ -155,15 +160,15 @@ QCancelButton.defaultProps = {
|
||||
|
||||
interface QSubmitButtonProps
|
||||
{
|
||||
label?: string
|
||||
iconName?: string
|
||||
disabled: boolean
|
||||
label?: string;
|
||||
iconName?: string;
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
export function QSubmitButton({label, iconName, disabled}: QSubmitButtonProps): JSX.Element
|
||||
{
|
||||
return (
|
||||
<Box ml={3} width={standardWidth}>
|
||||
<Box ml={standardML} width={standardWidth}>
|
||||
<MDButton type="submit" variant="gradient" color="dark" size="small" fullWidth startIcon={<Icon>{iconName}</Icon>} disabled={disabled}>
|
||||
{label}
|
||||
</MDButton>
|
||||
|
@ -172,14 +172,10 @@ function QDynamicForm({formData, formLabel, bulkEditMode, bulkEditSwitchChangeHa
|
||||
<Grid item xs={12} sm={6} key={fieldName}>
|
||||
{labelElement}
|
||||
<DynamicSelect
|
||||
tableName={field.possibleValueProps.tableName}
|
||||
processName={field.possibleValueProps.processName}
|
||||
possibleValueSourceName={field.possibleValueProps.possibleValueSourceName}
|
||||
fieldName={field.possibleValueProps.fieldName}
|
||||
fieldPossibleValueProps={field.possibleValueProps}
|
||||
isEditable={field.isEditable}
|
||||
fieldLabel=""
|
||||
initialValue={values[fieldName]}
|
||||
initialDisplayValue={field.possibleValueProps.initialDisplayValue}
|
||||
bulkEditMode={bulkEditMode}
|
||||
bulkEditSwitchChangeHandler={bulkEditSwitchChanged}
|
||||
otherValues={otherValuesMap}
|
||||
|
@ -40,6 +40,8 @@ interface Props
|
||||
value: any;
|
||||
type: string;
|
||||
isEditable?: boolean;
|
||||
placeholder?: string;
|
||||
backgroundColor?: string;
|
||||
|
||||
[key: string]: any;
|
||||
|
||||
@ -49,7 +51,7 @@ interface Props
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
const [switchChecked, setSwitchChecked] = useState(false);
|
||||
@ -65,18 +67,30 @@ function QDynamicFormField({
|
||||
inputLabelProps.shrink = true;
|
||||
}
|
||||
|
||||
const inputProps = {};
|
||||
const inputProps: any = {};
|
||||
if (displayFormat && displayFormat.startsWith("$"))
|
||||
{
|
||||
// @ts-ignore
|
||||
inputProps.startAdornment = <InputAdornment position="start">$</InputAdornment>;
|
||||
}
|
||||
if (displayFormat && displayFormat.endsWith("%%"))
|
||||
{
|
||||
// @ts-ignore
|
||||
inputProps.endAdornment = <InputAdornment position="end">%</InputAdornment>;
|
||||
}
|
||||
|
||||
if (placeholder)
|
||||
{
|
||||
inputProps.placeholder = placeholder
|
||||
}
|
||||
|
||||
if(backgroundColor)
|
||||
{
|
||||
inputProps.sx = {
|
||||
"&.MuiInputBase-root": {
|
||||
backgroundColor: backgroundColor
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const handleOnWheel = (e) =>
|
||||
{
|
||||
|
@ -22,6 +22,7 @@
|
||||
import {AdornmentType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/AdornmentType";
|
||||
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
|
||||
import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType";
|
||||
import {FieldPossibleValueProps} from "qqq/models/fields/FieldPossibleValueProps";
|
||||
import * as Yup from "yup";
|
||||
|
||||
|
||||
@ -129,18 +130,11 @@ class DynamicFormUtils
|
||||
|
||||
if (effectivelyIsRequired)
|
||||
{
|
||||
if (field.possibleValueSourceName)
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// the "nullable(true)" here doesn't mean that you're allowed to set the field to null... //
|
||||
// rather, it's more like "null is how empty will be treated" or some-such... //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
return (Yup.string().required(`${field.label} is required.`).nullable(true));
|
||||
}
|
||||
else
|
||||
{
|
||||
return (Yup.string().required(`${field.label} is required.`));
|
||||
}
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// the "nullable(true)" here doesn't mean that you're allowed to set the field to null... //
|
||||
// rather, it's more like "null is how empty will be treated" or some-such... //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
return (Yup.string().required(`${field.label ?? "This field"} is required.`).nullable(true));
|
||||
}
|
||||
return (null);
|
||||
}
|
||||
@ -155,47 +149,49 @@ class DynamicFormUtils
|
||||
{
|
||||
const field = qFields[i];
|
||||
|
||||
if(!dynamicFormFields[field.name])
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
/////////////////////////////////////////
|
||||
// add props for possible value fields //
|
||||
/////////////////////////////////////////
|
||||
if (field.possibleValueSourceName && dynamicFormFields[field.name])
|
||||
if (field.possibleValueSourceName || field.inlinePossibleValueSource)
|
||||
{
|
||||
let initialDisplayValue = null;
|
||||
let props: FieldPossibleValueProps =
|
||||
{
|
||||
isPossibleValue: true,
|
||||
fieldName: field.name,
|
||||
initialDisplayValue: null
|
||||
}
|
||||
|
||||
if (displayValues)
|
||||
{
|
||||
initialDisplayValue = displayValues.get(field.name);
|
||||
props.initialDisplayValue = displayValues.get(field.name);
|
||||
}
|
||||
|
||||
if (tableName)
|
||||
if(field.inlinePossibleValueSource)
|
||||
{
|
||||
dynamicFormFields[field.name].possibleValueProps =
|
||||
{
|
||||
isPossibleValue: true,
|
||||
tableName: tableName,
|
||||
fieldName: field.name,
|
||||
initialDisplayValue: initialDisplayValue,
|
||||
};
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// handle an inline PVS - which is a list of possible value objects //
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
props.possibleValues = field.inlinePossibleValueSource;
|
||||
}
|
||||
else if (tableName)
|
||||
{
|
||||
props.tableName = tableName;
|
||||
}
|
||||
else if (processName)
|
||||
{
|
||||
dynamicFormFields[field.name].possibleValueProps =
|
||||
{
|
||||
isPossibleValue: true,
|
||||
processName: processName,
|
||||
fieldName: field.name,
|
||||
initialDisplayValue: initialDisplayValue,
|
||||
};
|
||||
props.processName = processName;
|
||||
}
|
||||
else
|
||||
{
|
||||
dynamicFormFields[field.name].possibleValueProps =
|
||||
{
|
||||
isPossibleValue: true,
|
||||
initialDisplayValue: initialDisplayValue,
|
||||
fieldName: field.name,
|
||||
possibleValueSourceName: field.possibleValueSourceName
|
||||
};
|
||||
props.possibleValueSourceName = field.possibleValueSourceName;
|
||||
}
|
||||
|
||||
dynamicFormFields[field.name].possibleValueProps = props;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -30,20 +30,17 @@ import TextField from "@mui/material/TextField";
|
||||
import {ErrorMessage, useFormikContext} from "formik";
|
||||
import colors from "qqq/assets/theme/base/colors";
|
||||
import MDTypography from "qqq/components/legacy/MDTypography";
|
||||
import {FieldPossibleValueProps} from "qqq/models/fields/FieldPossibleValueProps";
|
||||
import Client from "qqq/utils/qqq/Client";
|
||||
import React, {useEffect, useState} from "react";
|
||||
|
||||
interface Props
|
||||
{
|
||||
tableName?: string;
|
||||
processName?: string;
|
||||
fieldName?: string;
|
||||
possibleValueSourceName?: string;
|
||||
fieldPossibleValueProps: FieldPossibleValueProps;
|
||||
overrideId?: string;
|
||||
fieldLabel: string;
|
||||
inForm: boolean;
|
||||
initialValue?: any;
|
||||
initialDisplayValue?: string;
|
||||
initialValues?: QPossibleValue[];
|
||||
onChange?: any;
|
||||
isEditable?: boolean;
|
||||
@ -57,13 +54,8 @@ interface Props
|
||||
}
|
||||
|
||||
DynamicSelect.defaultProps = {
|
||||
tableName: null,
|
||||
processName: null,
|
||||
fieldName: null,
|
||||
possibleValueSourceName: null,
|
||||
inForm: true,
|
||||
initialValue: null,
|
||||
initialDisplayValue: null,
|
||||
initialValues: undefined,
|
||||
onChange: null,
|
||||
isEditable: true,
|
||||
@ -103,8 +95,10 @@ export const getAutocompleteOutlinedStyle = (isDisabled: boolean) =>
|
||||
|
||||
const qController = Client.getInstance();
|
||||
|
||||
function DynamicSelect({tableName, processName, fieldName, possibleValueSourceName, overrideId, fieldLabel, inForm, initialValue, initialDisplayValue, initialValues, onChange, isEditable, isMultiple, bulkEditMode, bulkEditSwitchChangeHandler, otherValues, variant, initiallyOpen, useCase}: Props)
|
||||
function DynamicSelect({fieldPossibleValueProps, overrideId, fieldLabel, inForm, initialValue, initialValues, onChange, isEditable, isMultiple, bulkEditMode, bulkEditSwitchChangeHandler, otherValues, variant, initiallyOpen, useCase}: Props)
|
||||
{
|
||||
const {fieldName, initialDisplayValue, possibleValueSourceName, possibleValues, processName, tableName} = fieldPossibleValueProps;
|
||||
|
||||
const [open, setOpen] = useState(initiallyOpen);
|
||||
const [options, setOptions] = useState<readonly QPossibleValue[]>([]);
|
||||
const [searchTerm, setSearchTerm] = useState(null);
|
||||
@ -172,6 +166,35 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
||||
setFieldValueRef = setFieldValue;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
const filterInlinePossibleValues = (searchTerm: string, possibleValues: QPossibleValue[]): QPossibleValue[] =>
|
||||
{
|
||||
return possibleValues.filter(pv => pv.label?.toLowerCase().startsWith(searchTerm));
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
const loadResults = async (): Promise<QPossibleValue[]> =>
|
||||
{
|
||||
if(possibleValues)
|
||||
{
|
||||
return filterInlinePossibleValues(searchTerm, possibleValues)
|
||||
}
|
||||
else
|
||||
{
|
||||
return await qController.possibleValues(tableName, processName, possibleValueSourceName ?? fieldName, searchTerm ?? "", null, otherValues, useCase);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
useEffect(() =>
|
||||
{
|
||||
if (firstRender)
|
||||
@ -195,7 +218,7 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
||||
(async () =>
|
||||
{
|
||||
// console.log(`doing a search with ${searchTerm}`);
|
||||
const results: QPossibleValue[] = await qController.possibleValues(tableName, processName, possibleValueSourceName ?? fieldName, searchTerm ?? "", null, otherValues, useCase);
|
||||
const results: QPossibleValue[] = await loadResults();
|
||||
|
||||
if (tableMetaData == null && tableName)
|
||||
{
|
||||
@ -218,7 +241,10 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
||||
};
|
||||
}, [searchTerm]);
|
||||
|
||||
// todo - finish... call it in onOpen?
|
||||
|
||||
/***************************************************************************
|
||||
** todo - finish... call it in onOpen?
|
||||
***************************************************************************/
|
||||
const reloadIfOtherValuesAreChanged = () =>
|
||||
{
|
||||
if (JSON.stringify(Object.fromEntries(otherValues)) != otherValuesWhenResultsWereLoaded)
|
||||
@ -227,8 +253,10 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
||||
{
|
||||
setLoading(true);
|
||||
setOptions([]);
|
||||
|
||||
console.log("Refreshing possible values...");
|
||||
const results: QPossibleValue[] = await qController.possibleValues(tableName, processName, possibleValueSourceName ?? fieldName, searchTerm ?? "", null, otherValues, useCase);
|
||||
const results: QPossibleValue[] = await loadResults();
|
||||
|
||||
setLoading(false);
|
||||
setOptions([...results]);
|
||||
setOtherValuesWhenResultsWereLoaded(JSON.stringify(Object.fromEntries(otherValues)));
|
||||
@ -236,6 +264,10 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
const inputChanged = (event: React.SyntheticEvent, value: string, reason: string) =>
|
||||
{
|
||||
// console.log(`input changed. Reason: ${reason}, setting search term to ${value}`);
|
||||
@ -246,11 +278,19 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
const handleBlur = (x: any) =>
|
||||
{
|
||||
setSearchTerm(null);
|
||||
};
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
const handleChanged = (event: React.SyntheticEvent, value: any | any[], reason: string, details?: string) =>
|
||||
{
|
||||
// console.log("handleChanged. value is:");
|
||||
@ -274,6 +314,10 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
const filterOptions = (options: { id: any; label: string; }[], state: FilterOptionsState<{ id: any; label: string; }>): { id: any; label: string; }[] =>
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
@ -283,6 +327,10 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
||||
return (options);
|
||||
};
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
// @ts-ignore
|
||||
const renderOption = (props: Object, option: any, {selected}) =>
|
||||
{
|
||||
@ -331,6 +379,10 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
const bulkEditSwitchChanged = () =>
|
||||
{
|
||||
const newSwitchValue = !switchChecked;
|
||||
@ -351,7 +403,7 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
||||
const autocomplete = (
|
||||
<Box>
|
||||
<Autocomplete
|
||||
id={overrideId ?? fieldName ?? possibleValueSourceName}
|
||||
id={overrideId ?? fieldName ?? possibleValueSourceName ?? "anonymous"}
|
||||
sx={autocompleteSX}
|
||||
open={open}
|
||||
fullWidth
|
||||
@ -431,7 +483,7 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
||||
inForm &&
|
||||
<Box mt={0.75}>
|
||||
<MDTypography component="div" variant="caption" color="error" fontWeight="regular">
|
||||
{!isDisabled && <div className="fieldErrorMessage"><ErrorMessage name={fieldName ?? possibleValueSourceName} render={msg => <span data-field-error="true">{msg}</span>} /></div>}
|
||||
{!isDisabled && <div className="fieldErrorMessage"><ErrorMessage name={overrideId ?? fieldName ?? possibleValueSourceName ?? "anonymous"} render={msg => <span data-field-error="true">{msg}</span>} /></div>}
|
||||
</MDTypography>
|
||||
</Box>
|
||||
}
|
||||
|
@ -64,13 +64,14 @@ function Footer({company, links}: Props): JSX.Element
|
||||
<Box
|
||||
width="100%"
|
||||
display="flex"
|
||||
flexDirection={{xs: "column", lg: "row"}}
|
||||
flexDirection={{xs: "column", md: "row"}}
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
px={1.5}
|
||||
style={{
|
||||
position: "fixed", bottom: "0px", zIndex: -1, marginBottom: "10px",
|
||||
}}
|
||||
left={{xs: "0", xl: "auto"}}
|
||||
>
|
||||
{
|
||||
href && name &&
|
||||
|
@ -84,7 +84,7 @@ function ProcessSummaryResults({
|
||||
);
|
||||
|
||||
return (
|
||||
<Box m={3} mt={6}>
|
||||
<Box m={{xs: 0, md: 3}} mt={"3rem!important"}>
|
||||
<Grid container>
|
||||
<Grid item xs={0} lg={2} />
|
||||
<Grid item xs={12} lg={8}>
|
||||
|
@ -273,7 +273,7 @@ function ValidationReview({
|
||||
);
|
||||
|
||||
return (
|
||||
<Box m={3}>
|
||||
<Box m={{xs: 0, md: 3}} mt={"3rem!important"}>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12} lg={6}>
|
||||
<MDTypography color="body" variant="button">
|
||||
|
@ -367,13 +367,11 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC
|
||||
) : (
|
||||
<Box width={"100%"}>
|
||||
<DynamicSelect
|
||||
tableName={table.name}
|
||||
fieldName={field.name}
|
||||
fieldPossibleValueProps={{tableName: table.name, fieldName: field.name, initialDisplayValue: selectedPossibleValue?.label}}
|
||||
overrideId={field.name + "-single-" + criteria.id}
|
||||
key={field.name + "-single-" + criteria.id}
|
||||
fieldLabel="Value"
|
||||
initialValue={selectedPossibleValue?.id}
|
||||
initialDisplayValue={selectedPossibleValue?.label}
|
||||
inForm={false}
|
||||
onChange={(value: any) => valueChangeHandler(null, 0, value)}
|
||||
variant="standard"
|
||||
@ -402,8 +400,7 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC
|
||||
}
|
||||
return <Box>
|
||||
<DynamicSelect
|
||||
tableName={table.name}
|
||||
fieldName={field.name}
|
||||
fieldPossibleValueProps={{tableName: table.name, fieldName: field.name, initialDisplayValue: null}}
|
||||
overrideId={field.name + "-multi-" + criteria.id}
|
||||
key={field.name + "-multi-" + criteria.id}
|
||||
isMultiple
|
||||
|
@ -440,10 +440,10 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
|
||||
<Box sx={{height: openTool ? "45%" : "100%"}}>
|
||||
<Grid container alignItems="flex-end">
|
||||
<Box maxWidth={"50%"} minWidth={300}>
|
||||
<DynamicSelect fieldName={"apiName"} initialValue={apiName} initialDisplayValue={apiNameLabel} fieldLabel={"API Name *"} tableName={"scriptRevision"} inForm={false} onChange={changeApiName} useCase="form" />
|
||||
<DynamicSelect fieldPossibleValueProps={{tableName: "scriptRevision", fieldName: "apiName", initialDisplayValue: apiNameLabel}} initialValue={apiName} fieldLabel={"API Name *"} inForm={false} onChange={changeApiName} useCase="form" />
|
||||
</Box>
|
||||
<Box maxWidth={"50%"} minWidth={300} pl={2}>
|
||||
<DynamicSelect fieldName={"apiVersion"} initialValue={apiVersion} initialDisplayValue={apiVersionLabel} fieldLabel={"API Version *"} tableName={"scriptRevision"} inForm={false} onChange={changeApiVersion} useCase="form" />
|
||||
<DynamicSelect fieldPossibleValueProps={{tableName: "scriptRevision", fieldName: "apiVersion", initialDisplayValue: apiVersionLabel}} initialValue={apiVersion} fieldLabel={"API Version *"} inForm={false} onChange={changeApiVersion} useCase="form" />
|
||||
</Box>
|
||||
</Grid>
|
||||
<Box display="flex" sx={{height: "100%"}}>
|
||||
|
@ -391,10 +391,9 @@ export default function ShareModal({open, onClose, tableMetaData, record}: Share
|
||||
<Box display="flex" flexDirection="row" alignItems="center">
|
||||
<Box width="550px" pr={2} mb={-1.5}>
|
||||
<DynamicSelect
|
||||
possibleValueSourceName={shareableTableMetaData.audiencePossibleValueSourceName}
|
||||
fieldPossibleValueProps={{possibleValueSourceName: shareableTableMetaData.audiencePossibleValueSourceName, initialDisplayValue: selectedAudienceOption?.label}}
|
||||
fieldLabel="User or Group" // todo should come from shareableTableMetaData
|
||||
initialValue={selectedAudienceOption?.id}
|
||||
initialDisplayValue={selectedAudienceOption?.label}
|
||||
inForm={false}
|
||||
onChange={handleAudienceChange}
|
||||
useCase="form"
|
||||
|
@ -22,19 +22,25 @@
|
||||
|
||||
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
||||
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 {BlockData} from "qqq/components/widgets/blocks/BlockModels";
|
||||
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
|
||||
{
|
||||
blockId: string;
|
||||
blocks: BlockData[];
|
||||
styleOverrides?: any;
|
||||
layout?: string;
|
||||
overlayHtml?: string;
|
||||
overlayStyleOverrides?: any;
|
||||
modalMode: string;
|
||||
styles?: any;
|
||||
}
|
||||
|
||||
|
||||
@ -42,13 +48,15 @@ interface CompositeWidgetProps
|
||||
{
|
||||
widgetMetaData: QWidgetMetaData;
|
||||
data: CompositeData;
|
||||
actionCallback?: (blockData: BlockData, eventValues?: { [name: string]: any }) => boolean;
|
||||
values?: { [key: string]: any };
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Widget which is a list of Blocks.
|
||||
*******************************************************************************/
|
||||
export default function CompositeWidget({widgetMetaData, data}: CompositeWidgetProps): JSX.Element
|
||||
export default function CompositeWidget({widgetMetaData, data, actionCallback, values}: CompositeWidgetProps): JSX.Element
|
||||
{
|
||||
if (!data || !data.blocks)
|
||||
{
|
||||
@ -74,6 +82,12 @@ export default function CompositeWidget({widgetMetaData, data}: CompositeWidgetP
|
||||
boxStyle.flexWrap = "wrap";
|
||||
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")
|
||||
{
|
||||
boxStyle.display = "flex";
|
||||
@ -81,6 +95,14 @@ export default function CompositeWidget({widgetMetaData, data}: CompositeWidgetP
|
||||
boxStyle.justifyContent = "space-between";
|
||||
boxStyle.gap = "0.25rem";
|
||||
}
|
||||
else if (layout == "FLEX_ROW_CENTER")
|
||||
{
|
||||
boxStyle.display = "flex";
|
||||
boxStyle.flexDirection = "row";
|
||||
boxStyle.justifyContent = "center";
|
||||
boxStyle.gap = "0.25rem";
|
||||
boxStyle.flexWrap = "wrap";
|
||||
}
|
||||
else if (layout == "TABLE_SUB_ROW_DETAILS")
|
||||
{
|
||||
boxStyle.display = "flex";
|
||||
@ -105,6 +127,19 @@ export default function CompositeWidget({widgetMetaData, data}: CompositeWidgetP
|
||||
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 = {};
|
||||
|
||||
if (data?.overlayStyleOverrides)
|
||||
@ -112,7 +147,7 @@ export default function CompositeWidget({widgetMetaData, data}: CompositeWidgetP
|
||||
overlayStyle = {...overlayStyle, ...data.overlayStyleOverrides};
|
||||
}
|
||||
|
||||
return (
|
||||
const content = (
|
||||
<>
|
||||
{
|
||||
data?.overlayHtml &&
|
||||
@ -122,7 +157,7 @@ export default function CompositeWidget({widgetMetaData, data}: CompositeWidgetP
|
||||
{
|
||||
data.blocks.map((block: BlockData, index) => (
|
||||
<React.Fragment key={index}>
|
||||
<WidgetBlock widgetMetaData={widgetMetaData} block={block} />
|
||||
<WidgetBlock widgetMetaData={widgetMetaData} block={block} actionCallback={actionCallback} values={values} />
|
||||
</React.Fragment>
|
||||
))
|
||||
}
|
||||
@ -130,4 +165,53 @@ export default function CompositeWidget({widgetMetaData, data}: CompositeWidgetP
|
||||
</>
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -29,6 +29,7 @@ import parse from "html-react-parser";
|
||||
import QContext from "QContext";
|
||||
import MDTypography from "qqq/components/legacy/MDTypography";
|
||||
import TabPanel from "qqq/components/misc/TabPanel";
|
||||
import {BlockData} from "qqq/components/widgets/blocks/BlockModels";
|
||||
import BarChart from "qqq/components/widgets/charts/barchart/BarChart";
|
||||
import HorizontalBarChart from "qqq/components/widgets/charts/barchart/HorizontalBarChart";
|
||||
import DefaultLineChart from "qqq/components/widgets/charts/linechart/DefaultLineChart";
|
||||
@ -71,6 +72,9 @@ interface Props
|
||||
childUrlParams?: string;
|
||||
parentWidgetMetaData?: QWidgetMetaData;
|
||||
wrapWidgetsInTabPanels: boolean;
|
||||
actionCallback?: (blockData: BlockData) => boolean;
|
||||
initialWidgetDataList: any[];
|
||||
values?: {[key: string]: any};
|
||||
}
|
||||
|
||||
DashboardWidgets.defaultProps = {
|
||||
@ -82,11 +86,14 @@ DashboardWidgets.defaultProps = {
|
||||
childUrlParams: "",
|
||||
parentWidgetMetaData: null,
|
||||
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 [, forceUpdate] = useReducer((x) => x + 1, 0);
|
||||
|
||||
@ -114,7 +121,15 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
|
||||
|
||||
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([]);
|
||||
|
||||
for (let i = 0; i < widgetMetaDataList.length; i++)
|
||||
{
|
||||
const widgetMetaData = widgetMetaDataList[i];
|
||||
@ -563,7 +578,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
|
||||
labelAdditionalComponentsRight={labelAdditionalComponentsRight}
|
||||
labelAdditionalComponentsLeft={labelAdditionalComponentsLeft}
|
||||
>
|
||||
<CompositeWidget widgetMetaData={widgetMetaData} data={widgetData[i]} />
|
||||
<CompositeWidget widgetMetaData={widgetMetaData} data={widgetData[i]} actionCallback={actionCallback} values={values} />
|
||||
</Widget>
|
||||
)
|
||||
}
|
||||
|
@ -22,6 +22,9 @@
|
||||
|
||||
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
||||
import {Alert, Skeleton} from "@mui/material";
|
||||
import ButtonBlock from "qqq/components/widgets/blocks/ButtonBlock";
|
||||
import AudioBlock from "qqq/components/widgets/blocks/AudioBlock";
|
||||
import InputFieldBlock from "qqq/components/widgets/blocks/InputFieldBlock";
|
||||
import React from "react";
|
||||
import BigNumberBlock from "qqq/components/widgets/blocks/BigNumberBlock";
|
||||
import {BlockData} from "qqq/components/widgets/blocks/BlockModels";
|
||||
@ -32,19 +35,22 @@ import TableSubRowDetailRowBlock from "qqq/components/widgets/blocks/TableSubRow
|
||||
import TextBlock from "qqq/components/widgets/blocks/TextBlock";
|
||||
import UpOrDownNumberBlock from "qqq/components/widgets/blocks/UpOrDownNumberBlock";
|
||||
import CompositeWidget from "qqq/components/widgets/CompositeWidget";
|
||||
import ImageBlock from "./blocks/ImageBlock";
|
||||
|
||||
|
||||
interface WidgetBlockProps
|
||||
{
|
||||
widgetMetaData: QWidgetMetaData;
|
||||
block: BlockData;
|
||||
actionCallback?: (blockData: BlockData, eventValues?: {[name: string]: any}) => boolean;
|
||||
values?: { [key: string]: any };
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Component to render a single Block in the widget framework!
|
||||
*******************************************************************************/
|
||||
export default function WidgetBlock({widgetMetaData, block}: WidgetBlockProps): JSX.Element
|
||||
export default function WidgetBlock({widgetMetaData, block, actionCallback, values}: WidgetBlockProps): JSX.Element
|
||||
{
|
||||
if(!block)
|
||||
{
|
||||
@ -64,7 +70,7 @@ export default function WidgetBlock({widgetMetaData, block}: WidgetBlockProps):
|
||||
if(block.blockTypeName == "COMPOSITE")
|
||||
{
|
||||
// @ts-ignore - special case for composite type block...
|
||||
return (<CompositeWidget widgetMetaData={widgetMetaData} data={block} />);
|
||||
return (<CompositeWidget widgetMetaData={widgetMetaData} data={block} actionCallback={actionCallback} values={values} />);
|
||||
}
|
||||
|
||||
switch(block.blockTypeName)
|
||||
@ -83,6 +89,14 @@ export default function WidgetBlock({widgetMetaData, block}: WidgetBlockProps):
|
||||
return (<DividerBlock widgetMetaData={widgetMetaData} data={block} />);
|
||||
case "BIG_NUMBER":
|
||||
return (<BigNumberBlock widgetMetaData={widgetMetaData} data={block} />);
|
||||
case "INPUT_FIELD":
|
||||
return (<InputFieldBlock widgetMetaData={widgetMetaData} data={block} actionCallback={actionCallback} />);
|
||||
case "BUTTON":
|
||||
return (<ButtonBlock widgetMetaData={widgetMetaData} data={block} actionCallback={actionCallback} />);
|
||||
case "AUDIO":
|
||||
return (<AudioBlock widgetMetaData={widgetMetaData} data={block} />);
|
||||
case "IMAGE":
|
||||
return (<ImageBlock widgetMetaData={widgetMetaData} data={block} actionCallback={actionCallback} />);
|
||||
default:
|
||||
return (<Alert sx={{m: "0.5rem"}} color="warning">Unsupported block type: {block.blockTypeName}</Alert>)
|
||||
}
|
||||
|
40
src/qqq/components/widgets/blocks/AudioBlock.tsx
Normal file
40
src/qqq/components/widgets/blocks/AudioBlock.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. 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 Box from "@mui/material/Box";
|
||||
import BlockElementWrapper from "qqq/components/widgets/blocks/BlockElementWrapper";
|
||||
import {StandardBlockComponentProps} from "qqq/components/widgets/blocks/BlockModels";
|
||||
import DumpJsonBox from "qqq/utils/DumpJsonBox";
|
||||
import React from "react";
|
||||
|
||||
/*******************************************************************************
|
||||
** Block that renders ... an audio tag
|
||||
**
|
||||
** <audio src=${path} ${autoPlay} ${showControls} />
|
||||
*******************************************************************************/
|
||||
export default function AudioBlock({widgetMetaData, data}: StandardBlockComponentProps): JSX.Element
|
||||
{
|
||||
return (
|
||||
<BlockElementWrapper metaData={widgetMetaData} data={data} slot="">
|
||||
<audio src={data.values?.path} autoPlay={data.values?.autoPlay} controls={data.values?.showControls} />
|
||||
</BlockElementWrapper>
|
||||
);
|
||||
}
|
@ -35,6 +35,8 @@ export interface BlockData
|
||||
|
||||
values: any;
|
||||
styles?: any;
|
||||
|
||||
conditional?: string;
|
||||
}
|
||||
|
||||
|
||||
@ -57,5 +59,6 @@ export interface StandardBlockComponentProps
|
||||
{
|
||||
widgetMetaData: QWidgetMetaData;
|
||||
data: BlockData;
|
||||
actionCallback?: (blockData: BlockData, eventValues?: {[name: string]: any}) => boolean;
|
||||
}
|
||||
|
||||
|
86
src/qqq/components/widgets/blocks/ButtonBlock.tsx
Normal file
86
src/qqq/components/widgets/blocks/ButtonBlock.tsx
Normal file
@ -0,0 +1,86 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. 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 Box from "@mui/material/Box";
|
||||
import Icon from "@mui/material/Icon";
|
||||
import {standardWidth} from "qqq/components/buttons/DefaultButtons";
|
||||
import MDButton from "qqq/components/legacy/MDButton";
|
||||
import BlockElementWrapper from "qqq/components/widgets/blocks/BlockElementWrapper";
|
||||
import {StandardBlockComponentProps} from "qqq/components/widgets/blocks/BlockModels";
|
||||
import React from "react";
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Block that renders ... a button...
|
||||
**
|
||||
*******************************************************************************/
|
||||
export default function ButtonBlock({widgetMetaData, data, actionCallback}: StandardBlockComponentProps): JSX.Element
|
||||
{
|
||||
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()
|
||||
{
|
||||
if (actionCallback)
|
||||
{
|
||||
actionCallback(data, data.values);
|
||||
}
|
||||
else
|
||||
{
|
||||
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 (
|
||||
<BlockElementWrapper metaData={widgetMetaData} data={data} slot="">
|
||||
<Box mx={1} my={1} minWidth={standardWidth}>
|
||||
<MDButton
|
||||
type="button"
|
||||
variant={buttonVariant}
|
||||
color="dark"
|
||||
size="small"
|
||||
fullWidth
|
||||
startIcon={startIcon}
|
||||
endIcon={endIcon}
|
||||
onClick={onClick}
|
||||
>
|
||||
{data.values.label ?? "Button"}
|
||||
</MDButton>
|
||||
</Box>
|
||||
</BlockElementWrapper>
|
||||
);
|
||||
}
|
59
src/qqq/components/widgets/blocks/ImageBlock.tsx
Normal file
59
src/qqq/components/widgets/blocks/ImageBlock.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. 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 Box from "@mui/material/Box";
|
||||
import BlockElementWrapper from "qqq/components/widgets/blocks/BlockElementWrapper";
|
||||
import {StandardBlockComponentProps} from "qqq/components/widgets/blocks/BlockModels";
|
||||
import DumpJsonBox from "qqq/utils/DumpJsonBox";
|
||||
import React from "react";
|
||||
|
||||
/*******************************************************************************
|
||||
** Block that renders ... an image tag
|
||||
**
|
||||
** <audio src=${path} ${autoPlay} ${showControls} />
|
||||
*******************************************************************************/
|
||||
export default function AudioBlock({widgetMetaData, data}: StandardBlockComponentProps): JSX.Element
|
||||
{
|
||||
let imageStyle: any = {};
|
||||
|
||||
if(data.styles?.width)
|
||||
{
|
||||
imageStyle.width = data.styles?.width;
|
||||
}
|
||||
|
||||
if(data.styles?.height)
|
||||
{
|
||||
imageStyle.height = data.styles?.height;
|
||||
}
|
||||
|
||||
if(data.styles?.bordered)
|
||||
{
|
||||
imageStyle.border = "1px solid #C0C0C0";
|
||||
imageStyle.borderRadius = "0.5rem";
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<BlockElementWrapper metaData={widgetMetaData} data={data} slot="">
|
||||
<img src={data.values?.path} alt={data.values?.alt} style={imageStyle} />
|
||||
</BlockElementWrapper>
|
||||
);
|
||||
}
|
139
src/qqq/components/widgets/blocks/InputFieldBlock.tsx
Normal file
139
src/qqq/components/widgets/blocks/InputFieldBlock.tsx
Normal file
@ -0,0 +1,139 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. 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 {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
|
||||
import Box from "@mui/material/Box";
|
||||
import QDynamicFormField from "qqq/components/forms/DynamicFormField";
|
||||
import DynamicFormUtils from "qqq/components/forms/DynamicFormUtils";
|
||||
import BlockElementWrapper from "qqq/components/widgets/blocks/BlockElementWrapper";
|
||||
import {StandardBlockComponentProps} from "qqq/components/widgets/blocks/BlockModels";
|
||||
import React, {SyntheticEvent, useState} from "react";
|
||||
|
||||
/*******************************************************************************
|
||||
** Block that renders ... a text input
|
||||
**
|
||||
*******************************************************************************/
|
||||
export default function InputFieldBlock({widgetMetaData, data, actionCallback}: StandardBlockComponentProps): JSX.Element
|
||||
{
|
||||
const [blurCount, setBlurCount] = useState(0)
|
||||
|
||||
const fieldMetaData = new QFieldMetaData(data.values.fieldMetaData);
|
||||
const dynamicField = DynamicFormUtils.getDynamicField(fieldMetaData);
|
||||
|
||||
let autoFocus = data.values.autoFocus as boolean
|
||||
let value = data.values.value;
|
||||
if(value == null || value == undefined)
|
||||
{
|
||||
value = "";
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// for an autoFocus field... //
|
||||
// we're finding that if we blur it when clicking an action button, that //
|
||||
// an un-desirable "now it's been touched, so show an error" happens. //
|
||||
// so let us remove the default blur handler, for the first (auto) focus/blur //
|
||||
// cycle, and we seem to have a better time. //
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
let dynamicFormFieldRest: {onBlur?: any, sx?: any} = {}
|
||||
if(autoFocus && blurCount == 0)
|
||||
{
|
||||
dynamicFormFieldRest.onBlur = (event: React.SyntheticEvent) =>
|
||||
{
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
setBlurCount(blurCount + 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
function eventHandler(event: KeyboardEvent)
|
||||
{
|
||||
if(data.values.submitOnEnter && event.key == "Enter")
|
||||
{
|
||||
// @ts-ignore target.value...
|
||||
const inputValue = event.target.value?.trim()
|
||||
|
||||
// todo - make this behavior opt-in for inputBlocks?
|
||||
if(inputValue && `${inputValue}`.startsWith("->"))
|
||||
{
|
||||
const actionCode = inputValue.substring(2);
|
||||
if(actionCallback)
|
||||
{
|
||||
actionCallback(data, {actionCode: actionCode, _fieldToClearIfError: fieldMetaData.name});
|
||||
|
||||
///////////////////////////////////////////////////////
|
||||
// return, so we don't submit the actionCode as text //
|
||||
///////////////////////////////////////////////////////
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if(fieldMetaData.isRequired && inputValue == "")
|
||||
{
|
||||
console.log("input field is required, but missing value, so not submitting");
|
||||
return;
|
||||
}
|
||||
|
||||
if(actionCallback)
|
||||
{
|
||||
console.log("InputFieldBlock calling actionCallback for submitOnEnter");
|
||||
|
||||
let values: {[name: string]: any} = {};
|
||||
values[fieldMetaData.name] = inputValue;
|
||||
|
||||
actionCallback(data, values);
|
||||
}
|
||||
else
|
||||
{
|
||||
console.log("InputFieldBlock was set as submitOnEnter, but no actionCallback was present, so, noop");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const labelElement = <Box fontSize="1rem" fontWeight="500" marginBottom="0.25rem">
|
||||
<label htmlFor={fieldMetaData.name}>{fieldMetaData.label}</label>
|
||||
</Box>
|
||||
|
||||
return (
|
||||
<Box mt="0.5rem">
|
||||
<BlockElementWrapper metaData={widgetMetaData} data={data} slot="">
|
||||
<>
|
||||
{labelElement}
|
||||
<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>
|
||||
</Box>
|
||||
);
|
||||
}
|
@ -19,8 +19,12 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Box from "@mui/material/Box";
|
||||
import Icon from "@mui/material/Icon";
|
||||
import BlockElementWrapper from "qqq/components/widgets/blocks/BlockElementWrapper";
|
||||
import {StandardBlockComponentProps} from "qqq/components/widgets/blocks/BlockModels";
|
||||
import ProcessWidgetBlockUtils from "qqq/pages/processes/ProcessWidgetBlockUtils";
|
||||
import React from "react";
|
||||
|
||||
/*******************************************************************************
|
||||
** Block that renders ... just some text.
|
||||
@ -29,9 +33,132 @@ import {StandardBlockComponentProps} from "qqq/components/widgets/blocks/BlockMo
|
||||
*******************************************************************************/
|
||||
export default function TextBlock({widgetMetaData, data}: StandardBlockComponentProps): JSX.Element
|
||||
{
|
||||
let color = "rgba(0, 0, 0, 0.87)";
|
||||
if (data.styles?.color)
|
||||
{
|
||||
color = ProcessWidgetBlockUtils.processColorFromStyleMap(data.styles.color);
|
||||
}
|
||||
|
||||
let boxStyle = {};
|
||||
if (data.styles?.format == "alert")
|
||||
{
|
||||
boxStyle =
|
||||
{
|
||||
border: `1px solid ${color}`,
|
||||
background: `${color}40`,
|
||||
padding: "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 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 (
|
||||
<BlockElementWrapper metaData={widgetMetaData} data={data} slot="">
|
||||
<span style={{fontSize: "1.000rem"}}>{data.values.text}</span>
|
||||
<Box display="inline-block" lineHeight="1.2" sx={boxStyle}>
|
||||
<span style={{fontSize: fontSize, color: color, fontWeight: fontWeight}}>
|
||||
{lines.map((line: string, index: number) =>
|
||||
(
|
||||
<div key={index}>
|
||||
<>
|
||||
{index == 0 && startIcon ? {startIcon} : null}
|
||||
{line}
|
||||
{index == lines.length - 1 && endIcon ? {endIcon} : null}
|
||||
</>
|
||||
</div>
|
||||
))
|
||||
}</span>
|
||||
</Box>
|
||||
</BlockElementWrapper>
|
||||
);
|
||||
}
|
||||
|
Reference in New Issue
Block a user