diff --git a/src/qqq/components/processes/BulkLoadFileMappingField.tsx b/src/qqq/components/processes/BulkLoadFileMappingField.tsx index ac06871..1e6d82b 100644 --- a/src/qqq/components/processes/BulkLoadFileMappingField.tsx +++ b/src/qqq/components/processes/BulkLoadFileMappingField.tsx @@ -21,9 +21,10 @@ import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData"; -import {Checkbox, FormControlLabel, Radio} from "@mui/material"; +import {Checkbox, FormControlLabel, Radio, Tooltip} 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"; @@ -45,6 +46,27 @@ interface BulkLoadMappingFieldProps forceParentUpdate?: () => void, } +const xIconButtonSX = + { + border: `1px solid ${colors.grayLines.main} !important`, + borderRadius: "0.5rem", + textTransform: "none", + fontSize: "1rem", + fontWeight: "400", + width: "30px", + minWidth: "30px", + height: "2rem", + minHeight: "2rem", + paddingLeft: 0, + paddingRight: 0, + marginRight: "0.5rem", + marginTop: "0.5rem", + color: colors.error.main, + "&:hover": {color: colors.error.main}, + "&:focus": {color: colors.error.main}, + "&:focus:not(:hover)": {color: colors.error.main}, + }; + const qController = Client.getInstance(); /*************************************************************************** @@ -212,7 +234,9 @@ export default function BulkLoadFileMappingField({bulkLoadField, isRequired, rem { - (!isRequired) && removeFieldCallback()} sx={{pt: "0.75rem"}}>remove_circle + (!isRequired) && + + } {bulkLoadField.getQualifiedLabel()} @@ -265,7 +289,7 @@ export default function BulkLoadFileMappingField({bulkLoadField, isRequired, rem { bulkLoadField.error && - + {bulkLoadField.error} } diff --git a/src/qqq/components/processes/BulkLoadFileMappingFields.tsx b/src/qqq/components/processes/BulkLoadFileMappingFields.tsx index f8dd561..0aa74cf 100644 --- a/src/qqq/components/processes/BulkLoadFileMappingFields.tsx +++ b/src/qqq/components/processes/BulkLoadFileMappingFields.tsx @@ -47,7 +47,7 @@ export default function BulkLoadFileMappingFields({bulkLoadMapping, fileDescript { const [, forceUpdate] = useReducer((x) => x + 1, 0); - const [forceRerender, setForceRerender] = useState(0); + const [forceHierarchyAutoCompleteRerender, setForceHierarchyAutoCompleteRerender] = useState(0); //////////////////////////////////////////// // build list of fields that can be added // @@ -98,8 +98,9 @@ export default function BulkLoadFileMappingFields({bulkLoadMapping, fileDescript setAddFieldsDisableStates(newDisableStates); setTooltips(newTooltips); + setForceHierarchyAutoCompleteRerender(forceHierarchyAutoCompleteRerender + 1); - }, [bulkLoadMapping]); + }, [bulkLoadMapping, bulkLoadMapping.layout]); /////////////////////////////////////////////// @@ -140,9 +141,6 @@ export default function BulkLoadFileMappingFields({bulkLoadMapping, fileDescript ***************************************************************************/ function removeField(bulkLoadField: BulkLoadField) { - // addFieldsToggleStates[bulkLoadField.getQualifiedName()] = false; - // setAddFieldsToggleStates(Object.assign({}, addFieldsToggleStates)); - addFieldsDisableStates[bulkLoadField.getQualifiedName()] = false; setAddFieldsDisableStates(Object.assign({}, addFieldsDisableStates)); @@ -160,7 +158,7 @@ export default function BulkLoadFileMappingFields({bulkLoadMapping, fileDescript bulkLoadMapping.removeField(bulkLoadField); forceUpdate(); forceParentUpdate(); - setForceRerender(forceRerender + 1); + setForceHierarchyAutoCompleteRerender(forceHierarchyAutoCompleteRerender + 1); } /*************************************************************************** @@ -297,7 +295,7 @@ export default function BulkLoadFileMappingFields({bulkLoadMapping, fileDescript isModeSelectOne keepOpenAfterSelectOne handleSelectedOption={handleAddField} - forceRerender={forceRerender} + forceRerender={forceHierarchyAutoCompleteRerender} disabledStates={addFieldsDisableStates} tooltips={tooltips} /> diff --git a/src/qqq/components/processes/BulkLoadFileMappingForm.tsx b/src/qqq/components/processes/BulkLoadFileMappingForm.tsx index 709f768..bacf001 100644 --- a/src/qqq/components/processes/BulkLoadFileMappingForm.tsx +++ b/src/qqq/components/processes/BulkLoadFileMappingForm.tsx @@ -26,10 +26,12 @@ import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstan import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData"; import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData"; import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord"; +import {Badge, Icon} from "@mui/material"; 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 Tooltip from "@mui/material/Tooltip/Tooltip"; import {useFormikContext} from "formik"; import {DynamicFormFieldLabel} from "qqq/components/forms/DynamicForm"; import QDynamicFormField from "qqq/components/forms/DynamicFormField"; @@ -126,6 +128,14 @@ const BulkLoadFileMappingForm = forwardRef(({processValues, tableMetaData, metaD } setFieldErrors(fieldErrors); + if(haveProfileErrors) + { + setTimeout(() => + { + document.querySelector(".bulkLoadFieldError")?.scrollIntoView({behavior: "smooth", block: "center", inline: "center"}); + }, 250); + } + return {maySubmit: !haveProfileErrors && !haveLocalErrors, values}; } }; @@ -224,7 +234,11 @@ const BulkLoadFileMappingForm = forwardRef(({processValues, tableMetaData, metaD forceUpdate()} + forceParentUpdate={() => + { + setRerenderHeader(rerenderHeader + 1); + forceUpdate(); + }} /> @@ -293,7 +307,7 @@ function BulkLoadMappingHeader({fileDescription, fileName, bulkLoadMapping, fiel ***************************************************************************/ function layoutChanged(event: any, newValue: any) { - bulkLoadMapping.layout = newValue ? newValue.id : null; + bulkLoadMapping.switchLayout(newValue ? newValue.id : null); fieldErrors.layout = null; forceParentUpdate(); } @@ -322,7 +336,7 @@ function BulkLoadMappingHeader({fileDescription, fileName, bulkLoadMapping, fiel
File Details
- + @@ -347,6 +361,7 @@ function BulkLoadMappingHeader({fileDescription, fileName, bulkLoadMapping, fiel getOptionLabel={(option) => typeof (option) == "string" ? option : (option?.label ?? "")} isOptionEqualToValue={(option, value) => option == null && value == null || option.id == value.id} renderOption={(props, option, state) => (
  • {option?.label ?? ""}
  • )} + disableClearable sx={{"& .MuiOutlinedInput-root": {padding: "0"}}} /> { @@ -366,13 +381,14 @@ function BulkLoadMappingHeader({fileDescription, fileName, bulkLoadMapping, fiel interface BulkLoadMappingFilePreviewProps { - fileDescription: FileDescription; + fileDescription: FileDescription, + bulkLoadMapping?: BulkLoadMapping } /*************************************************************************** ** private subcomponent - the file-preview section of the bulk load file mapping screen. ***************************************************************************/ -function BulkLoadMappingFilePreview({fileDescription}: BulkLoadMappingFilePreviewProps): JSX.Element +function BulkLoadMappingFilePreview({fileDescription, bulkLoadMapping}: BulkLoadMappingFilePreviewProps): JSX.Element { const rows: number[] = []; for (let i = 0; i < fileDescription.bodyValuesPreview[0].length; i++) @@ -380,25 +396,135 @@ function BulkLoadMappingFilePreview({fileDescription}: BulkLoadMappingFilePrevie rows.push(i); } + /*************************************************************************** + ** + ***************************************************************************/ + function getValue(i: number, j: number) + { + const value = fileDescription.bodyValuesPreview[j][i]; + if (value == null) + { + return ""; + } + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // this was useful at one point in time when we had an object coming back for xlsx files with many different data types // + // we'd see a .string attribute, which would have the value we'd want to show. not using it now, but keep in case // + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // @ts-ignore + if (value && value.string) + { + // @ts-ignore + return (value.string); + } + return `${value}`; + } + + /*************************************************************************** + ** + ***************************************************************************/ + function getHeaderColor(count: number): string + { + if (count > 0) + { + return "blue"; + } + + return "black"; + } + + /*************************************************************************** + ** + ***************************************************************************/ + function getCursor(count: number): string + { + if (count > 0) + { + return "pointer"; + } + + return "default"; + } + + /*************************************************************************** + ** + ***************************************************************************/ + function getColumnTooltip(fields: BulkLoadField[]) + { + return ( + This column is mapped to the field{fields.length == 1 ? "" : "s"}: +
      + {fields.map((field, i) =>
    • {field.getQualifiedLabel()}
    • )} +
    +
    ); + } + return ( - + - {fileDescription.headerLetters.map((letter) => )} + {fileDescription.headerLetters.map((letter, index) => + { + const fields = bulkLoadMapping.getFieldsForColumnIndex(index); + const count = fields.length; + return (); + })} - {fileDescription.headerValues.map((value) => )} + + {fileDescription.headerValues.map((value, index) => + { + const fields = bulkLoadMapping.getFieldsForColumnIndex(index); + const count = fields.length; + const tdStyle = {color: getHeaderColor(count), cursor: getCursor(count), backgroundColor: ""}; + + if(fileDescription.hasHeaderRow) + { + tdStyle.backgroundColor = "#ebebeb"; + + if(count > 0) + { + return + } + else + { + return + } + } + else + { + return + } + } + )} {rows.map((i) => ( - {fileDescription.headerLetters.map((letter, j) => )} + {fileDescription.headerLetters.map((letter, j) => )} ))} diff --git a/src/qqq/models/processes/BulkLoadModels.ts b/src/qqq/models/processes/BulkLoadModels.ts index e2e43f0..92e2c74 100644 --- a/src/qqq/models/processes/BulkLoadModels.ts +++ b/src/qqq/models/processes/BulkLoadModels.ts @@ -21,6 +21,7 @@ import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData"; +import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType"; import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord"; export type ValueType = "defaultValue" | "column"; @@ -422,17 +423,22 @@ export class BulkLoadMapping } else { - index = 0; - /////////////////////////////////////////////////////////// - // count how many copies of this field there are already // - /////////////////////////////////////////////////////////// + /////////////////////////////////////////////// + // find the max index for this field already // + /////////////////////////////////////////////// + let maxIndex = -1; for (let existingField of [...this.requiredFields, ...this.additionalFields]) { if (existingField.getQualifiedName() == bulkLoadField.getQualifiedName()) { - index++; + const thisIndex = existingField.wideLayoutIndexPath[0] + if (thisIndex != null && thisIndex != undefined && thisIndex > maxIndex) + { + maxIndex = thisIndex; + } } } + index = maxIndex + 1; } const cloneField = BulkLoadField.clone(bulkLoadField); @@ -455,7 +461,7 @@ export class BulkLoadMapping const newAdditionalFields: BulkLoadField[] = []; for (let bulkLoadField of this.additionalFields) { - if (bulkLoadField.getQualifiedName() != toRemove.getQualifiedName()) + if (bulkLoadField.getQualifiedNameWithWideSuffix() != toRemove.getQualifiedNameWithWideSuffix()) { newAdditionalFields.push(bulkLoadField); } @@ -463,6 +469,107 @@ export class BulkLoadMapping this.additionalFields = newAdditionalFields; } + + + /*************************************************************************** + ** + ***************************************************************************/ + public switchLayout(newLayout: string): void + { + const newAdditionalFields: BulkLoadField[] = []; + let anyChanges = false; + + if ("WIDE" != newLayout) + { + //////////////////////////////////////////////////////////////////////////////////////////////////////// + // if going to a layout other than WIDE, make sure there aren't any fields with a wideLayoutIndexPath // + //////////////////////////////////////////////////////////////////////////////////////////////////////// + const namesWhereOneWideLayoutIndexHasBeenFound: { [name: string]: boolean } = {}; + for (let existingField of this.additionalFields) + { + if (existingField.wideLayoutIndexPath.length > 0) + { + const name = existingField.getQualifiedName(); + if (namesWhereOneWideLayoutIndexHasBeenFound[name]) + { + ///////////////////////////////////////////////////////////////////////////////////////////////////////// + // in this case, we're on like the 2nd or 3rd instance of, say, Line Item: SKU - so - just discard it. // + ///////////////////////////////////////////////////////////////////////////////////////////////////////// + anyChanges = true; + } + else + { + //////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // else, this is the 1st instance of, say, Line Item: SKU - so mark that we've found it - and keep this field // + // (that is, put it in the new array), but with no index path // + //////////////////////////////////////////////////////////////////////////////////////////////////////////////// + namesWhereOneWideLayoutIndexHasBeenFound[name] = true; + const newField = BulkLoadField.clone(existingField); + newField.wideLayoutIndexPath = []; + newAdditionalFields.push(newField) + anyChanges = true; + } + } + else + { + ////////////////////////////////////////////////////// + // else, non-wide-path fields, just get added as-is // + ////////////////////////////////////////////////////// + newAdditionalFields.push(existingField) + } + } + } + else + { + /////////////////////////////////////////////////////////////////////////////////////////////// + // if going to WIDE layout, then any field from a child table needs a wide-layout-index-path // + /////////////////////////////////////////////////////////////////////////////////////////////// + for (let existingField of this.additionalFields) + { + if (existingField.tableStructure.isMain) + { + //////////////////////////////////////////// + // fields from main table come over as-is // + //////////////////////////////////////////// + newAdditionalFields.push(existingField) + } + else + { + ///////////////////////////////////////////////////////////////////////////////////////////// + // fields from child tables get a wideLayoutIndexPath (and we're assuming just 1 for each) // + ///////////////////////////////////////////////////////////////////////////////////////////// + const newField = BulkLoadField.clone(existingField); + newField.wideLayoutIndexPath = [0]; + newAdditionalFields.push(newField) + anyChanges = true; + } + } + } + + if (anyChanges) + { + this.additionalFields = newAdditionalFields; + } + } + + + /*************************************************************************** + ** + ***************************************************************************/ + public getFieldsForColumnIndex(i: number): BulkLoadField[] + { + const rs: BulkLoadField[] = []; + + for (let field of [...this.requiredFields, ...this.additionalFields]) + { + if(field.valueType == "column" && field.columnIndex == i) + { + rs.push(field); + } + } + + return (rs); + } } @@ -517,21 +624,85 @@ export class FileDescription /*************************************************************************** ** ***************************************************************************/ - public getPreviewValues(columnIndex: number): string[] + public getPreviewValues(columnIndex: number, fieldType?: QFieldType): string[] { if (columnIndex == undefined) { return []; } - if (this.hasHeaderRow) + function getTypedValue(value: any): string { - return (this.bodyValuesPreview[columnIndex]); + if(value == null) + { + return ""; + } + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // this was useful at one point in time when we had an object coming back for xlsx files with many different data types // + // we'd see a .string attribute, which would have the value we'd want to show. not using it now, but keep in case // + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + if (value && value.string) + { + switch (fieldType) + { + case QFieldType.BOOLEAN: + { + return value.bool; + } + + case QFieldType.STRING: + case QFieldType.TEXT: + case QFieldType.HTML: + case QFieldType.PASSWORD: + { + return value.string; + } + + case QFieldType.INTEGER: + case QFieldType.LONG: + { + return value.integer; + } + case QFieldType.DECIMAL: + { + return value.decimal; + } + case QFieldType.DATE: + { + return value.date; + } + case QFieldType.TIME: + { + return value.time; + } + case QFieldType.DATE_TIME: + { + return value.dateTime; + } + case QFieldType.BLOB: + return ""; // !! + } + } + + return (`${value}`); } - else + + const valueArray: string[] = []; + + if (!this.hasHeaderRow) { - return ([this.headerValues[columnIndex], ...this.bodyValuesPreview[columnIndex]]); + const typedValue = getTypedValue(this.headerValues[columnIndex]) + valueArray.push(typedValue == null ? "" : `${typedValue}`); } + + for (let value of this.bodyValuesPreview[columnIndex]) + { + const typedValue = getTypedValue(value) + valueArray.push(typedValue == null ? "" : `${typedValue}`); + } + + return (valueArray); } }
    {letter} + <> + { + count > 0 && + + + {letter} + + + + } + { + count == 0 && {letter} + } + +
    1{value} + {value} + {value}{value}
    {i + 2}{fileDescription.bodyValuesPreview[j][i]}{getValue(i, j)}