mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-18 13:20:43 +00:00
CE-1955 - Initial checkin of qfmd support for bulk-load
This commit is contained in:
226
src/qqq/components/processes/BulkLoadFileMappingField.tsx
Normal file
226
src/qqq/components/processes/BulkLoadFileMappingField.tsx
Normal file
@ -0,0 +1,226 @@
|
||||
/*
|
||||
* 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 {Checkbox, FormControlLabel, Radio} from "@mui/material";
|
||||
import Autocomplete from "@mui/material/Autocomplete";
|
||||
import Box from "@mui/material/Box";
|
||||
import Button from "@mui/material/Button";
|
||||
import Icon from "@mui/material/Icon";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import RadioGroup from "@mui/material/RadioGroup";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import {useFormikContext} from "formik";
|
||||
import colors from "qqq/assets/theme/base/colors";
|
||||
import QDynamicFormField from "qqq/components/forms/DynamicFormField";
|
||||
import DynamicFormUtils from "qqq/components/forms/DynamicFormUtils";
|
||||
import {BulkLoadField, FileDescription} from "qqq/models/processes/BulkLoadModels";
|
||||
import React, {useEffect, useState} from "react";
|
||||
|
||||
interface BulkLoadMappingFieldProps
|
||||
{
|
||||
bulkLoadField: BulkLoadField,
|
||||
isRequired: boolean,
|
||||
removeFieldCallback?: () => void,
|
||||
fileDescription: FileDescription,
|
||||
forceParentUpdate?: () => void,
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
** row for a single field on the bulk load mapping screen.
|
||||
***************************************************************************/
|
||||
export default function BulkLoadFileMappingField({bulkLoadField, isRequired, removeFieldCallback, fileDescription, forceParentUpdate}: BulkLoadMappingFieldProps): JSX.Element
|
||||
{
|
||||
const columnNames = fileDescription.getColumnNames();
|
||||
|
||||
const [valueType, setValueType] = useState(bulkLoadField.valueType);
|
||||
const [selectedColumn, setSelectedColumn] = useState({label: columnNames[bulkLoadField.columnIndex], value: bulkLoadField.columnIndex});
|
||||
|
||||
const fieldMetaData = new QFieldMetaData(bulkLoadField.field);
|
||||
const dynamicField = DynamicFormUtils.getDynamicField(fieldMetaData);
|
||||
const dynamicFieldInObject: any = {};
|
||||
dynamicFieldInObject[fieldMetaData["name"]] = dynamicField;
|
||||
DynamicFormUtils.addPossibleValueProps(dynamicFieldInObject, [fieldMetaData], bulkLoadField.tableStructure.tableName, null, null);
|
||||
|
||||
const columnOptions: { value: number, label: string }[] = [];
|
||||
for (let i = 0; i < columnNames.length; i++)
|
||||
{
|
||||
columnOptions.push({label: columnNames[i], value: i});
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// try to pick up changes in the hasHeaderRow toggle from way above //
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
if(bulkLoadField.columnIndex != null && bulkLoadField.columnIndex != undefined && selectedColumn.label && columnNames[bulkLoadField.columnIndex] != selectedColumn.label)
|
||||
{
|
||||
setSelectedColumn({label: columnNames[bulkLoadField.columnIndex], value: bulkLoadField.columnIndex})
|
||||
}
|
||||
|
||||
const mainFontSize = "0.875rem";
|
||||
const smallerFontSize = "0.75rem";
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// some field types get their value from formik. //
|
||||
// so for a pre-populated value, do an on-load useEffect, that'll set the value in formik. //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
const {setFieldValue} = useFormikContext();
|
||||
useEffect(() =>
|
||||
{
|
||||
if (valueType == "defaultValue")
|
||||
{
|
||||
setFieldValue(`${bulkLoadField.field.name}.defaultValue`, bulkLoadField.defaultValue);
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
function columnChanged(event: any, newValue: any, reason: string)
|
||||
{
|
||||
setSelectedColumn(newValue);
|
||||
bulkLoadField.columnIndex = newValue == null ? null : newValue.value;
|
||||
|
||||
if (fileDescription.hasHeaderRow)
|
||||
{
|
||||
bulkLoadField.headerName = newValue == null ? null : newValue.label;
|
||||
}
|
||||
|
||||
bulkLoadField.error = null;
|
||||
forceParentUpdate && forceParentUpdate();
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
function defaultValueChanged(newValue: any)
|
||||
{
|
||||
setFieldValue(`${bulkLoadField.field.name}.defaultValue`, newValue);
|
||||
bulkLoadField.defaultValue = newValue;
|
||||
bulkLoadField.error = null;
|
||||
forceParentUpdate && forceParentUpdate();
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
function valueTypeChanged(isColumn: boolean)
|
||||
{
|
||||
const newValueType = isColumn ? "column" : "defaultValue";
|
||||
bulkLoadField.valueType = newValueType;
|
||||
setValueType(newValueType);
|
||||
bulkLoadField.error = null;
|
||||
forceParentUpdate && forceParentUpdate();
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
function mapValuesChanged(value: boolean)
|
||||
{
|
||||
bulkLoadField.doValueMapping = value;
|
||||
forceParentUpdate && forceParentUpdate();
|
||||
}
|
||||
|
||||
return (<Box py="0.5rem" sx={{borderBottom: "1px solid lightgray", width: "100%", overflow: "auto"}}>
|
||||
<Box display="grid" gridTemplateColumns="200px 400px auto" fontSize="1rem" gap="0.5rem" sx={
|
||||
{
|
||||
"& .MuiFormControlLabel-label": {ml: "0 !important", fontWeight: "normal !important", fontSize: mainFontSize}
|
||||
}}>
|
||||
|
||||
<Box display="flex" alignItems="flex-start">
|
||||
{
|
||||
(!isRequired) && <IconButton onClick={() => removeFieldCallback()} sx={{pt: "0.75rem"}}><Icon fontSize="small">remove_circle</Icon></IconButton>
|
||||
}
|
||||
<Box pt="0.625rem">
|
||||
{bulkLoadField.getQualifiedLabel()}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<RadioGroup name="valueType" value={valueType}>
|
||||
<Box>
|
||||
<Box display="flex" alignItems="center" sx={{height: "45px"}}>
|
||||
<FormControlLabel value="column" control={<Radio size="small" onChange={(event, checked) => valueTypeChanged(checked)} />} label={"File column"} sx={{minWidth: "140px", whiteSpace: "nowrap"}} />
|
||||
{
|
||||
valueType == "column" && <Box width="100%">
|
||||
<Autocomplete
|
||||
id={bulkLoadField.field.name}
|
||||
renderInput={(params) => (<TextField {...params} label={""} value={selectedColumn?.label} fullWidth variant="outlined" autoComplete="off" type="search" InputProps={{...params.InputProps}} sx={{"& .MuiOutlinedInput-root": {borderRadius: "0.75rem"}}} />)}
|
||||
fullWidth
|
||||
options={columnOptions}
|
||||
multiple={false}
|
||||
defaultValue={selectedColumn}
|
||||
value={selectedColumn}
|
||||
inputValue={selectedColumn?.label}
|
||||
onChange={columnChanged}
|
||||
getOptionLabel={(option) => typeof (option) == "string" ? option : (option?.label ?? "")}
|
||||
isOptionEqualToValue={(option, value) => option == null && value == null || option.value == value.value}
|
||||
renderOption={(props, option, state) => (<li {...props}>{option?.label ?? ""}</li>)}
|
||||
sx={{"& .MuiOutlinedInput-root": {padding: "0"}}}
|
||||
/>
|
||||
</Box>
|
||||
}
|
||||
</Box>
|
||||
<Box display="flex" alignItems="center" sx={{height: "45px"}}>
|
||||
<FormControlLabel value="defaultValue" control={<Radio size="small" onChange={(event, checked) => valueTypeChanged(!checked)} />} label={"Default value"} sx={{minWidth: "140px", whiteSpace: "nowrap"}} />
|
||||
{
|
||||
valueType == "defaultValue" && <Box width="100%">
|
||||
<QDynamicFormField
|
||||
name={`${bulkLoadField.field.name}.defaultValue`}
|
||||
displayFormat={""}
|
||||
label={""}
|
||||
formFieldObject={dynamicField}
|
||||
type={dynamicField.type}
|
||||
value={bulkLoadField.defaultValue}
|
||||
onChangeCallback={defaultValueChanged}
|
||||
/>
|
||||
</Box>
|
||||
}
|
||||
</Box>
|
||||
</Box>
|
||||
{
|
||||
bulkLoadField.error &&
|
||||
<Box fontSize={smallerFontSize} color={colors.error.main} ml="145px">
|
||||
{bulkLoadField.error}
|
||||
</Box>
|
||||
}
|
||||
</RadioGroup>
|
||||
|
||||
<Box ml="1rem">
|
||||
{
|
||||
valueType == "column" && <>
|
||||
<Box>
|
||||
<FormControlLabel value="mapValues" control={<Checkbox size="small" defaultChecked={bulkLoadField.doValueMapping} onChange={(event, checked) => mapValuesChanged(checked)} />} label={"Map values"} sx={{minWidth: "140px", whiteSpace: "nowrap"}} />
|
||||
</Box>
|
||||
<Box fontSize={mainFontSize} mt="0.5rem">
|
||||
Preview Values: <span style={{color: "gray"}}>{(fileDescription.getPreviewValues(selectedColumn?.value) ?? [""]).join(", ")}</span>
|
||||
</Box>
|
||||
</>
|
||||
}
|
||||
</Box>
|
||||
|
||||
</Box>
|
||||
</Box>);
|
||||
}
|
322
src/qqq/components/processes/BulkLoadFileMappingFields.tsx
Normal file
322
src/qqq/components/processes/BulkLoadFileMappingFields.tsx
Normal file
@ -0,0 +1,322 @@
|
||||
/*
|
||||
* 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 colors from "qqq/assets/theme/base/colors";
|
||||
import QHierarchyAutoComplete, {Group, Option} from "qqq/components/misc/QHierarchyAutoComplete";
|
||||
import BulkLoadFileMappingField from "qqq/components/processes/BulkLoadFileMappingField";
|
||||
import {BulkLoadField, BulkLoadMapping, FileDescription} from "qqq/models/processes/BulkLoadModels";
|
||||
import React, {useEffect, useReducer, useState} from "react";
|
||||
|
||||
interface BulkLoadMappingFieldsProps
|
||||
{
|
||||
bulkLoadMapping: BulkLoadMapping,
|
||||
fileDescription: FileDescription,
|
||||
forceParentUpdate?: () => void,
|
||||
}
|
||||
|
||||
|
||||
const ADD_SINGLE_FIELD_TOOLTIP = "Click to add this field to your mapping.";
|
||||
const ADD_MANY_FIELD_TOOLTIP = "Click to add this field to your mapping as many times as you need.";
|
||||
const ALREADY_ADDED_FIELD_TOOLTIP = "This field has already been added to your mapping.";
|
||||
|
||||
/***************************************************************************
|
||||
** The section of the bulk load mapping screen with all the fields.
|
||||
***************************************************************************/
|
||||
export default function BulkLoadFileMappingFields({bulkLoadMapping, fileDescription, forceParentUpdate}: BulkLoadMappingFieldsProps): JSX.Element
|
||||
{
|
||||
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
||||
|
||||
const [forceRerender, setForceRerender] = useState(0);
|
||||
|
||||
////////////////////////////////////////////
|
||||
// build list of fields that can be added //
|
||||
////////////////////////////////////////////
|
||||
const [addFieldsGroup, setAddFieldsGroup] = useState({
|
||||
label: bulkLoadMapping.tablesByPath[""]?.label,
|
||||
value: "mainTable",
|
||||
options: [],
|
||||
subGroups: []
|
||||
} as Group);
|
||||
// const [addFieldsToggleStates, setAddFieldsToggleStates] = useState({} as { [name: string]: boolean });
|
||||
const [addFieldsDisableStates, setAddFieldsDisableStates] = useState({} as { [name: string]: boolean });
|
||||
const [tooltips, setTooltips] = useState({} as { [name: string]: string });
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
const newDisableStates: { [name: string]: boolean } = {};
|
||||
const newTooltips: { [name: string]: string } = {};
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// do the unused fields array first, as we've got some use-case where i think a field from //
|
||||
// suggested mappings (or profiles?) are in this list, even though they shouldn't be? //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
for (let field of bulkLoadMapping.unusedFields)
|
||||
{
|
||||
const qualifiedName = field.getQualifiedName();
|
||||
newTooltips[qualifiedName] = field.isMany() ? ADD_MANY_FIELD_TOOLTIP : ADD_SINGLE_FIELD_TOOLTIP;
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////
|
||||
// then do all the required & additional fields //
|
||||
//////////////////////////////////////////////////
|
||||
for (let field of [...(bulkLoadMapping.requiredFields ?? []), ...(bulkLoadMapping.additionalFields ?? [])])
|
||||
{
|
||||
const qualifiedName = field.getQualifiedName();
|
||||
|
||||
if (bulkLoadMapping.layout == "WIDE" && field.isMany())
|
||||
{
|
||||
newDisableStates[qualifiedName] = false;
|
||||
newTooltips[qualifiedName] = ADD_MANY_FIELD_TOOLTIP;
|
||||
}
|
||||
else
|
||||
{
|
||||
newDisableStates[qualifiedName] = true;
|
||||
newTooltips[qualifiedName] = ALREADY_ADDED_FIELD_TOOLTIP;
|
||||
}
|
||||
}
|
||||
|
||||
setAddFieldsDisableStates(newDisableStates);
|
||||
setTooltips(newTooltips);
|
||||
|
||||
}, [bulkLoadMapping]);
|
||||
|
||||
|
||||
///////////////////////////////////////////////
|
||||
// initialize this structure on first render //
|
||||
///////////////////////////////////////////////
|
||||
if (addFieldsGroup.options.length == 0)
|
||||
{
|
||||
for (let qualifiedFieldName in bulkLoadMapping.fieldsByTablePrefix[""])
|
||||
{
|
||||
const bulkLoadField = bulkLoadMapping.fieldsByTablePrefix[""][qualifiedFieldName];
|
||||
const field = bulkLoadField.field;
|
||||
addFieldsGroup.options.push({label: field.label, value: field.name, bulkLoadField: bulkLoadField});
|
||||
}
|
||||
|
||||
for (let prefix in bulkLoadMapping.fieldsByTablePrefix)
|
||||
{
|
||||
if (prefix == "")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const associationOptions: Option[] = [];
|
||||
const tableStructure = bulkLoadMapping.tablesByPath[prefix];
|
||||
addFieldsGroup.subGroups.push({label: tableStructure.label, value: tableStructure.associationPath, options: associationOptions});
|
||||
|
||||
for (let qualifiedFieldName in bulkLoadMapping.fieldsByTablePrefix[prefix])
|
||||
{
|
||||
const bulkLoadField = bulkLoadMapping.fieldsByTablePrefix[prefix][qualifiedFieldName];
|
||||
const field = bulkLoadField.field;
|
||||
associationOptions.push({label: field.label, value: field.name, bulkLoadField: bulkLoadField});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
function removeField(bulkLoadField: BulkLoadField)
|
||||
{
|
||||
// addFieldsToggleStates[bulkLoadField.getQualifiedName()] = false;
|
||||
// setAddFieldsToggleStates(Object.assign({}, addFieldsToggleStates));
|
||||
|
||||
addFieldsDisableStates[bulkLoadField.getQualifiedName()] = false;
|
||||
setAddFieldsDisableStates(Object.assign({}, addFieldsDisableStates));
|
||||
|
||||
if (bulkLoadMapping.layout == "WIDE" && bulkLoadField.isMany())
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// ok, you can add more - so don't disable and don't change the tooltip //
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
}
|
||||
else
|
||||
{
|
||||
tooltips[bulkLoadField.getQualifiedName()] = ADD_SINGLE_FIELD_TOOLTIP;
|
||||
}
|
||||
|
||||
bulkLoadMapping.removeField(bulkLoadField);
|
||||
forceUpdate();
|
||||
forceParentUpdate();
|
||||
setForceRerender(forceRerender + 1);
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
function handleToggleField(option: Option, group: Group, newValue: boolean)
|
||||
{
|
||||
const fieldKey = group.value == "mainTable" ? option.value : group.value + "." + option.value;
|
||||
|
||||
// addFieldsToggleStates[fieldKey] = newValue;
|
||||
// setAddFieldsToggleStates(Object.assign({}, addFieldsToggleStates));
|
||||
|
||||
addFieldsDisableStates[fieldKey] = newValue;
|
||||
setAddFieldsDisableStates(Object.assign({}, addFieldsDisableStates));
|
||||
|
||||
const bulkLoadField = bulkLoadMapping.fields[fieldKey];
|
||||
if (bulkLoadField)
|
||||
{
|
||||
if (newValue)
|
||||
{
|
||||
bulkLoadMapping.addField(bulkLoadField);
|
||||
}
|
||||
else
|
||||
{
|
||||
bulkLoadMapping.removeField(bulkLoadField);
|
||||
}
|
||||
|
||||
forceUpdate();
|
||||
forceParentUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
function handleAddField(option: Option, group: Group)
|
||||
{
|
||||
const fieldKey = group.value == "mainTable" ? option.value : group.value + "." + option.value;
|
||||
|
||||
const bulkLoadField = bulkLoadMapping.fields[fieldKey];
|
||||
if (bulkLoadField)
|
||||
{
|
||||
bulkLoadMapping.addField(bulkLoadField);
|
||||
|
||||
// addFieldsDisableStates[fieldKey] = true;
|
||||
// setAddFieldsDisableStates(Object.assign({}, addFieldsDisableStates));
|
||||
|
||||
if (bulkLoadMapping.layout == "WIDE" && bulkLoadField.isMany())
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// ok, you can add more - so don't disable and don't change the tooltip //
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
}
|
||||
else
|
||||
{
|
||||
addFieldsDisableStates[fieldKey] = true;
|
||||
setAddFieldsDisableStates(Object.assign({}, addFieldsDisableStates));
|
||||
|
||||
tooltips[fieldKey] = ALREADY_ADDED_FIELD_TOOLTIP;
|
||||
}
|
||||
|
||||
forceUpdate();
|
||||
forceParentUpdate();
|
||||
|
||||
document.getElementById("addFieldsButton")?.scrollIntoView();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
function copyWideField(bulkLoadField: BulkLoadField)
|
||||
{
|
||||
bulkLoadMapping.addField(bulkLoadField);
|
||||
forceUpdate();
|
||||
//? //? forceParentUpdate();
|
||||
//? setForceRerender(forceRerender + 1);
|
||||
}
|
||||
|
||||
|
||||
let buttonBackground = "none";
|
||||
let buttonBorder = colors.grayLines.main;
|
||||
let buttonColor = colors.gray.main;
|
||||
|
||||
const addFieldMenuButtonStyles = {
|
||||
borderRadius: "0.75rem",
|
||||
border: `1px solid ${buttonBorder}`,
|
||||
color: buttonColor,
|
||||
textTransform: "none",
|
||||
fontWeight: 500,
|
||||
fontSize: "0.875rem",
|
||||
p: "0.5rem",
|
||||
backgroundColor: buttonBackground,
|
||||
"&:focus:not(:hover)": {
|
||||
color: buttonColor,
|
||||
backgroundColor: buttonBackground,
|
||||
},
|
||||
"&:hover": {
|
||||
color: buttonColor,
|
||||
backgroundColor: buttonBackground,
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<h5>Required Fields</h5>
|
||||
<Box pl={"1rem"}>
|
||||
{
|
||||
bulkLoadMapping.requiredFields.length == 0 &&
|
||||
<i style={{fontSize: "0.875rem"}}>There are no required fields in this table.</i>
|
||||
}
|
||||
{bulkLoadMapping.requiredFields.map((bulkLoadField) => (
|
||||
<BulkLoadFileMappingField
|
||||
fileDescription={fileDescription}
|
||||
key={bulkLoadField.getKey()}
|
||||
bulkLoadField={bulkLoadField}
|
||||
isRequired={true}
|
||||
forceParentUpdate={forceParentUpdate}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
<Box mt="1rem">
|
||||
<h5>Additional Fields</h5>
|
||||
<Box pl={"1rem"}>
|
||||
{bulkLoadMapping.additionalFields.map((bulkLoadField) => (
|
||||
<BulkLoadFileMappingField
|
||||
fileDescription={fileDescription}
|
||||
key={bulkLoadField.getKey()}
|
||||
bulkLoadField={bulkLoadField}
|
||||
isRequired={false}
|
||||
removeFieldCallback={() => removeField(bulkLoadField)}
|
||||
forceParentUpdate={forceParentUpdate}
|
||||
/>
|
||||
))}
|
||||
|
||||
<Box display="flex" pt="1rem" pl="12.5rem">
|
||||
<QHierarchyAutoComplete
|
||||
idPrefix="addFieldAutocomplete"
|
||||
defaultGroup={addFieldsGroup}
|
||||
menuDirection="up"
|
||||
buttonProps={{id: "addFieldsButton", sx: addFieldMenuButtonStyles}}
|
||||
buttonChildren={<><Icon sx={{mr: "0.5rem"}}>add</Icon> Add Fields <Icon sx={{ml: "0.5rem"}}>keyboard_arrow_down</Icon></>}
|
||||
isModeSelectOne
|
||||
keepOpenAfterSelectOne
|
||||
handleSelectedOption={handleAddField}
|
||||
forceRerender={forceRerender}
|
||||
disabledStates={addFieldsDisableStates}
|
||||
tooltips={tooltips}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
384
src/qqq/components/processes/BulkLoadFileMappingForm.tsx
Normal file
384
src/qqq/components/processes/BulkLoadFileMappingForm.tsx
Normal file
@ -0,0 +1,384 @@
|
||||
/*
|
||||
* 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 {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
|
||||
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
||||
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
||||
import Autocomplete from "@mui/material/Autocomplete";
|
||||
import Box from "@mui/material/Box";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import {useFormikContext} from "formik";
|
||||
import {DynamicFormFieldLabel} from "qqq/components/forms/DynamicForm";
|
||||
import QDynamicFormField from "qqq/components/forms/DynamicFormField";
|
||||
import MDTypography from "qqq/components/legacy/MDTypography";
|
||||
import SavedBulkLoadProfiles from "qqq/components/misc/SavedBulkLoadProfiles";
|
||||
import BulkLoadFileMappingFields from "qqq/components/processes/BulkLoadFileMappingFields";
|
||||
import {BulkLoadField, BulkLoadMapping, BulkLoadProfile, BulkLoadTableStructure, FileDescription, Wrapper} from "qqq/models/processes/BulkLoadModels";
|
||||
import {SubFormPreSubmitCallbackResultType} from "qqq/pages/processes/ProcessRun";
|
||||
import React, {forwardRef, useEffect, useImperativeHandle, useReducer, useState} from "react";
|
||||
import ProcessViewForm from "./ProcessViewForm";
|
||||
|
||||
|
||||
interface BulkLoadMappingFormProps
|
||||
{
|
||||
processValues: any;
|
||||
tableMetaData: QTableMetaData;
|
||||
metaData: QInstance;
|
||||
setActiveStepLabel: (label: string) => void;
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** process component - screen where user does a bulk-load file mapping.
|
||||
***************************************************************************/
|
||||
const BulkLoadFileMappingForm = forwardRef(({processValues, tableMetaData, metaData, setActiveStepLabel}: BulkLoadMappingFormProps, ref) =>
|
||||
{
|
||||
const {setFieldValue} = useFormikContext();
|
||||
|
||||
const [currentSavedBulkLoadProfile, setCurrentSavedBulkLoadProfile] = useState(null as QRecord);
|
||||
const [wrappedCurrentSavedBulkLoadProfile] = useState(new Wrapper<QRecord>(currentSavedBulkLoadProfile));
|
||||
|
||||
const [fieldErrors, setFieldErrors] = useState({} as { [fieldName: string]: string });
|
||||
|
||||
const [suggestedBulkLoadProfile] = useState(processValues.bulkLoadProfile as BulkLoadProfile);
|
||||
const [tableStructure] = useState(processValues.tableStructure as BulkLoadTableStructure);
|
||||
const [bulkLoadMapping, setBulkLoadMapping] = useState(BulkLoadMapping.fromBulkLoadProfile(tableStructure, suggestedBulkLoadProfile));
|
||||
const [wrappedBulkLoadMapping] = useState(new Wrapper<BulkLoadMapping>(bulkLoadMapping));
|
||||
|
||||
const [fileDescription] = useState(new FileDescription(processValues.headerValues, processValues.headerLetters, processValues.bodyValuesPreview));
|
||||
fileDescription.setHasHeaderRow(bulkLoadMapping.hasHeaderRow);
|
||||
|
||||
|
||||
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// ok - so - ... Autocomplete, at least as we're using it for the layout field - doesn't like //
|
||||
// to change its initial value. So, we want to work hard to force the Header sub-component to //
|
||||
// re-render upon external changes to the layout (e.g., new profile being selected). //
|
||||
// use this state-counter to make that happen (and let's please never speak of it again). //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
const [rerenderHeader, setRerenderHeader] = useState(1);
|
||||
|
||||
////////////////////////////////////////////////////////
|
||||
// ref-based callback for integration with ProcessRun //
|
||||
////////////////////////////////////////////////////////
|
||||
useImperativeHandle(ref, () =>
|
||||
{
|
||||
return {
|
||||
preSubmit(): SubFormPreSubmitCallbackResultType
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// convert the BulkLoadMapping to a BulkLoadProfile - the thing that the backend understands //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////
|
||||
const {haveErrors: haveProfileErrors, profile} = wrappedBulkLoadMapping.get().toProfile();
|
||||
|
||||
const values: { [name: string]: any } = {};
|
||||
|
||||
////////////////////////////////////////////////////
|
||||
// always re-submit the full profile //
|
||||
// note mostly a copy in BulkLoadValueMappingForm //
|
||||
////////////////////////////////////////////////////
|
||||
values["version"] = profile.version;
|
||||
values["fieldListJSON"] = JSON.stringify(profile.fieldList);
|
||||
values["savedBulkLoadProfileId"] = wrappedCurrentSavedBulkLoadProfile.get()?.values?.get("id");
|
||||
values["layout"] = wrappedBulkLoadMapping.get().layout;
|
||||
values["hasHeaderRow"] = wrappedBulkLoadMapping.get().hasHeaderRow;
|
||||
|
||||
let haveLocalErrors = false;
|
||||
const fieldErrors: { [fieldName: string]: string } = {};
|
||||
if (!values["layout"])
|
||||
{
|
||||
haveLocalErrors = true;
|
||||
fieldErrors["layout"] = "This field is required.";
|
||||
}
|
||||
|
||||
if (values["hasHeaderRow"] == null || values["hasHeaderRow"] == undefined)
|
||||
{
|
||||
haveLocalErrors = true;
|
||||
fieldErrors["hasHeaderRow"] = "This field is required.";
|
||||
}
|
||||
setFieldErrors(fieldErrors);
|
||||
|
||||
return {maySubmit: !haveProfileErrors && !haveLocalErrors, values};
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
console.log("@dk has header row changed!");
|
||||
}, [bulkLoadMapping.hasHeaderRow]);
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
function bulkLoadProfileOnChangeCallback(profileRecord: QRecord | null)
|
||||
{
|
||||
setCurrentSavedBulkLoadProfile(profileRecord);
|
||||
wrappedCurrentSavedBulkLoadProfile.set(profileRecord);
|
||||
|
||||
let newBulkLoadMapping: BulkLoadMapping;
|
||||
if (profileRecord)
|
||||
{
|
||||
newBulkLoadMapping = BulkLoadMapping.fromSavedProfileRecord(processValues.tableStructure, profileRecord);
|
||||
}
|
||||
else
|
||||
{
|
||||
newBulkLoadMapping = new BulkLoadMapping(processValues.tableStructure);
|
||||
}
|
||||
|
||||
handleNewBulkLoadMapping(newBulkLoadMapping);
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
function bulkLoadProfileResetToSuggestedMappingCallback()
|
||||
{
|
||||
handleNewBulkLoadMapping(BulkLoadMapping.fromBulkLoadProfile(processValues.tableStructure, suggestedBulkLoadProfile));
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
function handleNewBulkLoadMapping(newBulkLoadMapping: BulkLoadMapping)
|
||||
{
|
||||
const newRequiredFields: BulkLoadField[] = [];
|
||||
for (let field of newBulkLoadMapping.requiredFields)
|
||||
{
|
||||
newRequiredFields.push(BulkLoadField.clone(field));
|
||||
}
|
||||
newBulkLoadMapping.requiredFields = newRequiredFields;
|
||||
|
||||
setBulkLoadMapping(newBulkLoadMapping);
|
||||
wrappedBulkLoadMapping.set(newBulkLoadMapping);
|
||||
|
||||
setFieldValue("hasHeaderRow", newBulkLoadMapping.hasHeaderRow);
|
||||
setFieldValue("layout", newBulkLoadMapping.layout);
|
||||
|
||||
setRerenderHeader(rerenderHeader + 1);
|
||||
}
|
||||
|
||||
if (currentSavedBulkLoadProfile)
|
||||
{
|
||||
setActiveStepLabel(`File Mapping / ${currentSavedBulkLoadProfile.values.get("label")}`);
|
||||
}
|
||||
else
|
||||
{
|
||||
setActiveStepLabel("File Mapping");
|
||||
}
|
||||
|
||||
return (<Box>
|
||||
|
||||
<Box py="1rem" display="flex">
|
||||
<SavedBulkLoadProfiles
|
||||
metaData={metaData}
|
||||
tableMetaData={tableMetaData}
|
||||
tableStructure={tableStructure}
|
||||
currentSavedBulkLoadProfileRecord={currentSavedBulkLoadProfile}
|
||||
currentMapping={bulkLoadMapping}
|
||||
bulkLoadProfileOnChangeCallback={bulkLoadProfileOnChangeCallback}
|
||||
bulkLoadProfileResetToSuggestedMappingCallback={bulkLoadProfileResetToSuggestedMappingCallback}
|
||||
fileDescription={fileDescription}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<BulkLoadMappingHeader
|
||||
key={rerenderHeader}
|
||||
bulkLoadMapping={bulkLoadMapping}
|
||||
fileDescription={fileDescription}
|
||||
tableStructure={tableStructure}
|
||||
fileName={processValues.fileBaseName}
|
||||
fieldErrors={fieldErrors}
|
||||
forceParentUpdate={() => forceUpdate()}
|
||||
/>
|
||||
|
||||
<Box mt="2rem">
|
||||
<BulkLoadFileMappingFields
|
||||
bulkLoadMapping={bulkLoadMapping}
|
||||
fileDescription={fileDescription}
|
||||
forceParentUpdate={() => forceUpdate()}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
</Box>);
|
||||
|
||||
});
|
||||
|
||||
export default BulkLoadFileMappingForm;
|
||||
|
||||
|
||||
|
||||
|
||||
interface BulkLoadMappingHeaderProps
|
||||
{
|
||||
fileDescription: FileDescription,
|
||||
fileName: string,
|
||||
bulkLoadMapping?: BulkLoadMapping,
|
||||
fieldErrors: { [fieldName: string]: string },
|
||||
tableStructure: BulkLoadTableStructure,
|
||||
forceParentUpdate?: () => void
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
** private subcomponent - the header section of the bulk load file mapping screen.
|
||||
***************************************************************************/
|
||||
function BulkLoadMappingHeader({fileDescription, fileName, bulkLoadMapping, fieldErrors, tableStructure, forceParentUpdate}: BulkLoadMappingHeaderProps): JSX.Element
|
||||
{
|
||||
const viewFields = [
|
||||
new QFieldMetaData({name: "fileName", label: "File Name", type: "STRING"}),
|
||||
new QFieldMetaData({name: "fileDetails", label: "File Details", type: "STRING"}),
|
||||
];
|
||||
|
||||
const viewValues = {
|
||||
"fileName": fileName,
|
||||
"fileDetails": `${fileDescription.getColumnNames().length} column${fileDescription.getColumnNames().length == 1 ? "" : "s"}`
|
||||
};
|
||||
|
||||
const hasHeaderRowFormField = {name: "hasHeaderRow", label: "Does the file have a header row?", type: "checkbox", isRequired: true, isEditable: true};
|
||||
|
||||
let helpRoles = ["PROCESS_SCREEN", "ALL_SCREENS"];
|
||||
|
||||
const layoutOptions = [
|
||||
{label: "Flat", id: "FLAT"},
|
||||
{label: "Tall", id: "TALL"},
|
||||
{label: "Wide", id: "WIDE"},
|
||||
];
|
||||
|
||||
if (!tableStructure.associations)
|
||||
{
|
||||
layoutOptions.splice(1);
|
||||
}
|
||||
|
||||
const selectedLayout = layoutOptions.filter(o => o.id == bulkLoadMapping.layout)[0] ?? null;
|
||||
|
||||
function hasHeaderRowChanged(newValue: any)
|
||||
{
|
||||
bulkLoadMapping.hasHeaderRow = newValue;
|
||||
fileDescription.hasHeaderRow = newValue;
|
||||
fieldErrors.hasHeaderRow = null;
|
||||
forceParentUpdate();
|
||||
}
|
||||
|
||||
function layoutChanged(event: any, newValue: any)
|
||||
{
|
||||
bulkLoadMapping.layout = newValue ? newValue.id : null;
|
||||
fieldErrors.layout = null;
|
||||
forceParentUpdate();
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<h5>File Details</h5>
|
||||
<Box ml="1rem">
|
||||
<ProcessViewForm fields={viewFields} values={viewValues} columns={2} />
|
||||
<BulkLoadMappingFilePreview fileDescription={fileDescription} />
|
||||
<Grid container pt="1rem">
|
||||
<Grid item xs={12} md={6}>
|
||||
<DynamicFormFieldLabel name={hasHeaderRowFormField.name} label={`${hasHeaderRowFormField.label} *`} />
|
||||
<QDynamicFormField name={hasHeaderRowFormField.name} displayFormat={""} label={""} formFieldObject={hasHeaderRowFormField} type={"checkbox"} value={bulkLoadMapping.hasHeaderRow} onChangeCallback={hasHeaderRowChanged} />
|
||||
{
|
||||
fieldErrors.hasHeaderRow &&
|
||||
<MDTypography component="div" variant="caption" color="error" fontWeight="regular" mt="0.25rem">
|
||||
{<div className="fieldErrorMessage">{fieldErrors.hasHeaderRow}</div>}
|
||||
</MDTypography>
|
||||
}
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<DynamicFormFieldLabel name={"layout"} label={"File Layout *"} />
|
||||
<Autocomplete
|
||||
id={"layout"}
|
||||
renderInput={(params) => (<TextField {...params} label={""} fullWidth variant="outlined" autoComplete="off" type="search" InputProps={{...params.InputProps}} sx={{"& .MuiOutlinedInput-root": {borderRadius: "0.75rem"}}} />)}
|
||||
options={layoutOptions}
|
||||
multiple={false}
|
||||
defaultValue={selectedLayout}
|
||||
onChange={layoutChanged}
|
||||
getOptionLabel={(option) => typeof (option) == "string" ? option : (option?.label ?? "")}
|
||||
isOptionEqualToValue={(option, value) => option == null && value == null || option.id == value.id}
|
||||
renderOption={(props, option, state) => (<li {...props}>{option?.label ?? ""}</li>)}
|
||||
sx={{"& .MuiOutlinedInput-root": {padding: "0"}}}
|
||||
/>
|
||||
{
|
||||
fieldErrors.layout &&
|
||||
<MDTypography component="div" variant="caption" color="error" fontWeight="regular" mt="0.25rem">
|
||||
{<div className="fieldErrorMessage">{fieldErrors.layout}</div>}
|
||||
</MDTypography>
|
||||
}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
interface BulkLoadMappingFilePreviewProps
|
||||
{
|
||||
fileDescription: FileDescription;
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
** private subcomponent - the file-preview section of the bulk load file mapping screen.
|
||||
***************************************************************************/
|
||||
function BulkLoadMappingFilePreview({fileDescription}: BulkLoadMappingFilePreviewProps): JSX.Element
|
||||
{
|
||||
const rows: number[] = [];
|
||||
for (let i = 0; i < fileDescription.bodyValuesPreview[0].length; i++)
|
||||
{
|
||||
rows.push(i);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{"& table, & td": {border: "1px solid black", borderCollapse: "collapse", padding: "0 0.25rem", fontSize: "0.875rem", whiteSpace: "nowrap"}}}>
|
||||
<Box sx={{width: "100%", overflow: "auto"}}>
|
||||
<table cellSpacing="0" width="100%">
|
||||
<thead>
|
||||
<tr style={{backgroundColor: "#d3d3d3"}}>
|
||||
<td></td>
|
||||
{fileDescription.headerLetters.map((letter) => <td key={letter} style={{textAlign: "center"}}>{letter}</td>)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{backgroundColor: "#d3d3d3", textAlign: "center"}}>1</td>
|
||||
{fileDescription.headerValues.map((value) => <td key={value} style={{backgroundColor: fileDescription.hasHeaderRow ? "#ebebeb" : ""}}>{value}</td>)}
|
||||
</tr>
|
||||
{rows.map((i) => (
|
||||
<tr key={i}>
|
||||
<td style={{backgroundColor: "#d3d3d3", textAlign: "center"}}>{i + 2}</td>
|
||||
{fileDescription.headerLetters.map((letter, j) => <td key={j}>{fileDescription.bodyValuesPreview[j][i]}</td>)}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
|
102
src/qqq/components/processes/BulkLoadProfileForm.tsx
Normal file
102
src/qqq/components/processes/BulkLoadProfileForm.tsx
Normal file
@ -0,0 +1,102 @@
|
||||
/*
|
||||
* 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 {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
|
||||
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
||||
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
||||
import Box from "@mui/material/Box";
|
||||
import SavedBulkLoadProfiles from "qqq/components/misc/SavedBulkLoadProfiles";
|
||||
import {BulkLoadMapping, BulkLoadProfile, BulkLoadTableStructure, FileDescription, Wrapper} from "qqq/models/processes/BulkLoadModels";
|
||||
import {SubFormPreSubmitCallbackResultType} from "qqq/pages/processes/ProcessRun";
|
||||
import React, {forwardRef, useImperativeHandle, useState} from "react";
|
||||
|
||||
interface BulkLoadValueMappingFormProps
|
||||
{
|
||||
processValues: any,
|
||||
tableMetaData: QTableMetaData,
|
||||
metaData: QInstance
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** For review & result screens of bulk load - this process component shows
|
||||
** the SavedBulkLoadProfiles button.
|
||||
***************************************************************************/
|
||||
const BulkLoadProfileForm = forwardRef(({processValues, tableMetaData, metaData}: BulkLoadValueMappingFormProps, ref) =>
|
||||
{
|
||||
const savedBulkLoadProfileRecordProcessValue = processValues.savedBulkLoadProfileRecord;
|
||||
const [savedBulkLoadProfileRecord, setSavedBulkLoadProfileRecord] = useState(savedBulkLoadProfileRecordProcessValue == null ? null : new QRecord(savedBulkLoadProfileRecordProcessValue))
|
||||
|
||||
const [tableStructure] = useState(processValues.tableStructure as BulkLoadTableStructure);
|
||||
|
||||
const [bulkLoadProfile, setBulkLoadProfile] = useState(processValues.bulkLoadProfile as BulkLoadProfile);
|
||||
const [currentMapping, setCurrentMapping] = useState(BulkLoadMapping.fromBulkLoadProfile(tableStructure, bulkLoadProfile))
|
||||
const [wrappedCurrentSavedBulkLoadProfile] = useState(new Wrapper<QRecord>(savedBulkLoadProfileRecord));
|
||||
|
||||
const [fileDescription] = useState(new FileDescription(processValues.headerValues, processValues.headerLetters, processValues.bodyValuesPreview));
|
||||
fileDescription.setHasHeaderRow(currentMapping.hasHeaderRow);
|
||||
|
||||
useImperativeHandle(ref, () =>
|
||||
{
|
||||
return {
|
||||
preSubmit(): SubFormPreSubmitCallbackResultType
|
||||
{
|
||||
const values: { [name: string]: any } = {};
|
||||
values["savedBulkLoadProfileId"] = wrappedCurrentSavedBulkLoadProfile.get()?.values?.get("id");
|
||||
|
||||
return ({maySubmit: true, values});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
function bulkLoadProfileOnChangeCallback(profileRecord: QRecord | null)
|
||||
{
|
||||
setSavedBulkLoadProfileRecord(profileRecord);
|
||||
wrappedCurrentSavedBulkLoadProfile.set(profileRecord);
|
||||
|
||||
const newBulkLoadMapping = BulkLoadMapping.fromSavedProfileRecord(tableStructure, profileRecord);
|
||||
setCurrentMapping(newBulkLoadMapping);
|
||||
}
|
||||
|
||||
|
||||
return (<Box>
|
||||
|
||||
<Box py="1rem" display="flex">
|
||||
<SavedBulkLoadProfiles
|
||||
metaData={metaData}
|
||||
tableMetaData={tableMetaData}
|
||||
tableStructure={tableStructure}
|
||||
currentSavedBulkLoadProfileRecord={savedBulkLoadProfileRecord}
|
||||
currentMapping={currentMapping}
|
||||
allowSelectingProfile={false}
|
||||
fileDescription={fileDescription}
|
||||
bulkLoadProfileOnChangeCallback={bulkLoadProfileOnChangeCallback}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
</Box>);
|
||||
});
|
||||
|
||||
export default BulkLoadProfileForm;
|
222
src/qqq/components/processes/BulkLoadValueMappingForm.tsx
Normal file
222
src/qqq/components/processes/BulkLoadValueMappingForm.tsx
Normal file
@ -0,0 +1,222 @@
|
||||
/*
|
||||
* 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 {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
|
||||
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
||||
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
||||
import Box from "@mui/material/Box";
|
||||
import Icon from "@mui/material/Icon";
|
||||
import colors from "qqq/assets/theme/base/colors";
|
||||
import QDynamicFormField from "qqq/components/forms/DynamicFormField";
|
||||
import SavedBulkLoadProfiles from "qqq/components/misc/SavedBulkLoadProfiles";
|
||||
import {BulkLoadMapping, BulkLoadProfile, BulkLoadTableStructure, FileDescription, Wrapper} from "qqq/models/processes/BulkLoadModels";
|
||||
import {SubFormPreSubmitCallbackResultType} from "qqq/pages/processes/ProcessRun";
|
||||
import React, {forwardRef, useEffect, useImperativeHandle, useState} from "react";
|
||||
|
||||
interface BulkLoadValueMappingFormProps
|
||||
{
|
||||
processValues: any,
|
||||
setActiveStepLabel: (label: string) => void,
|
||||
tableMetaData: QTableMetaData,
|
||||
metaData: QInstance,
|
||||
formFields: any[]
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** process component used in bulk-load - on a screen that gets looped for
|
||||
** each field whose values are being mapped.
|
||||
***************************************************************************/
|
||||
const BulkLoadValueMappingForm = forwardRef(({processValues, setActiveStepLabel, tableMetaData, metaData, formFields}: BulkLoadValueMappingFormProps, ref) =>
|
||||
{
|
||||
const [field, setField] = useState(processValues.valueMappingField ? new QFieldMetaData(processValues.valueMappingField) : null);
|
||||
const [fieldFullName, setFieldFullName] = useState(processValues.valueMappingFullFieldName);
|
||||
const [fileValues, setFileValues] = useState((processValues.fileValues ?? []) as string[]);
|
||||
const [valueErrors, setValueErrors] = useState({} as { [fileValue: string]: any });
|
||||
|
||||
const [bulkLoadProfile, setBulkLoadProfile] = useState(processValues.bulkLoadProfile as BulkLoadProfile);
|
||||
|
||||
const savedBulkLoadProfileRecordProcessValue = processValues.savedBulkLoadProfileRecord;
|
||||
const [savedBulkLoadProfileRecord, setSavedBulkLoadProfileRecord] = useState(savedBulkLoadProfileRecordProcessValue == null ? null : new QRecord(savedBulkLoadProfileRecordProcessValue));
|
||||
const [wrappedCurrentSavedBulkLoadProfile] = useState(new Wrapper<QRecord>(savedBulkLoadProfileRecord));
|
||||
|
||||
const [tableStructure] = useState(processValues.tableStructure as BulkLoadTableStructure);
|
||||
|
||||
const [currentMapping, setCurrentMapping] = useState(initializeCurrentBulkLoadMapping());
|
||||
const [wrappedBulkLoadMapping] = useState(new Wrapper<BulkLoadMapping>(currentMapping));
|
||||
|
||||
const [fileDescription] = useState(new FileDescription(processValues.headerValues, processValues.headerLetters, processValues.bodyValuesPreview));
|
||||
fileDescription.setHasHeaderRow(currentMapping.hasHeaderRow);
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
function initializeCurrentBulkLoadMapping(): BulkLoadMapping
|
||||
{
|
||||
const bulkLoadMapping = BulkLoadMapping.fromBulkLoadProfile(tableStructure, bulkLoadProfile);
|
||||
|
||||
if (!bulkLoadMapping.valueMappings[fieldFullName])
|
||||
{
|
||||
bulkLoadMapping.valueMappings[fieldFullName] = {};
|
||||
}
|
||||
|
||||
return (bulkLoadMapping);
|
||||
}
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if (processValues.valueMappingField)
|
||||
{
|
||||
setField(new QFieldMetaData(processValues.valueMappingField));
|
||||
}
|
||||
else
|
||||
{
|
||||
setField(null);
|
||||
}
|
||||
}, [processValues.valueMappingField]);
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////
|
||||
// ref-based callback for integration with ProcessRun //
|
||||
////////////////////////////////////////////////////////
|
||||
useImperativeHandle(ref, () =>
|
||||
{
|
||||
return {
|
||||
preSubmit(): SubFormPreSubmitCallbackResultType
|
||||
{
|
||||
const values: { [name: string]: any } = {};
|
||||
|
||||
let anyErrors = false;
|
||||
const mappedValues = currentMapping.valueMappings[fieldFullName];
|
||||
if (field.isRequired)
|
||||
{
|
||||
for (let fileValue of fileValues)
|
||||
{
|
||||
valueErrors[fileValue] = null;
|
||||
if (mappedValues[fileValue] == null || mappedValues[fileValue] == undefined || mappedValues[fileValue] == "")
|
||||
{
|
||||
valueErrors[fileValue] = "A value is required for this mapping";
|
||||
anyErrors = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////
|
||||
// always re-submit the full profile //
|
||||
// note mostly a copy in BulkLoadFileMappingForm //
|
||||
///////////////////////////////////////////////////
|
||||
const {haveErrors, profile} = wrappedBulkLoadMapping.get().toProfile();
|
||||
values["version"] = profile.version;
|
||||
values["fieldListJSON"] = JSON.stringify(profile.fieldList);
|
||||
values["savedBulkLoadProfileId"] = wrappedCurrentSavedBulkLoadProfile.get()?.values?.get("id");
|
||||
values["layout"] = wrappedBulkLoadMapping.get().layout;
|
||||
values["hasHeaderRow"] = wrappedBulkLoadMapping.get().hasHeaderRow;
|
||||
|
||||
values["mappedValuesJSON"] = JSON.stringify(mappedValues);
|
||||
|
||||
return ({maySubmit: !anyErrors, values});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
if (!field)
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// this happens like between steps - render empty rather than a flash of half-stuff //
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
return (<Box></Box>);
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
function mappedValueChanged(fileValue: string, newValue: any)
|
||||
{
|
||||
valueErrors[fileValue] = null;
|
||||
currentMapping.valueMappings[fieldFullName][fileValue] = newValue;
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
function bulkLoadProfileOnChangeCallback(profileRecord: QRecord | null)
|
||||
{
|
||||
setSavedBulkLoadProfileRecord(profileRecord);
|
||||
wrappedCurrentSavedBulkLoadProfile.set(profileRecord);
|
||||
|
||||
const newBulkLoadMapping = BulkLoadMapping.fromSavedProfileRecord(tableStructure, profileRecord);
|
||||
setCurrentMapping(newBulkLoadMapping);
|
||||
wrappedBulkLoadMapping.set(newBulkLoadMapping);
|
||||
}
|
||||
|
||||
|
||||
setActiveStepLabel(`Value Mapping: ${field.label} (${processValues.valueMappingFieldIndex + 1} of ${processValues.fieldNamesToDoValueMapping?.length})`);
|
||||
|
||||
return (<Box>
|
||||
|
||||
<Box py="1rem" display="flex">
|
||||
<SavedBulkLoadProfiles
|
||||
metaData={metaData}
|
||||
tableMetaData={tableMetaData}
|
||||
tableStructure={tableStructure}
|
||||
currentSavedBulkLoadProfileRecord={savedBulkLoadProfileRecord}
|
||||
currentMapping={currentMapping}
|
||||
allowSelectingProfile={false}
|
||||
bulkLoadProfileOnChangeCallback={bulkLoadProfileOnChangeCallback}
|
||||
fileDescription={fileDescription}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{
|
||||
fileValues.map((fileValue, i) => (
|
||||
<Box key={i} py="0.5rem" sx={{borderBottom: "0px solid lightgray", width: "100%", overflow: "auto"}}>
|
||||
<Box display="grid" gridTemplateColumns="40% auto 60%" fontSize="1rem" gap="0.5rem">
|
||||
<Box mt="0.5rem" textAlign="right">{fileValue}</Box>
|
||||
<Box mt="0.625rem"><Icon>arrow_forward</Icon></Box>
|
||||
<Box maxWidth="300px">
|
||||
<QDynamicFormField
|
||||
name={`${fieldFullName}.value.${i}`}
|
||||
displayFormat={""}
|
||||
label={""}
|
||||
formFieldObject={formFields[i]}
|
||||
type={formFields[i].type}
|
||||
value={currentMapping.valueMappings[fieldFullName][fileValue]}
|
||||
onChangeCallback={(newValue) => mappedValueChanged(fileValue, newValue)}
|
||||
/>
|
||||
{
|
||||
valueErrors[fileValue] &&
|
||||
<Box fontSize={"0.875rem"} mt={"-0.75rem"} color={colors.error.main}>
|
||||
{valueErrors[fileValue]}
|
||||
</Box>
|
||||
}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
))
|
||||
}
|
||||
</Box>);
|
||||
|
||||
});
|
||||
|
||||
|
||||
export default BulkLoadValueMappingForm;
|
Reference in New Issue
Block a user