diff --git a/src/qqq/components/misc/SavedBulkLoadProfiles.tsx b/src/qqq/components/misc/SavedBulkLoadProfiles.tsx index b6137ec..0785031 100644 --- a/src/qqq/components/misc/SavedBulkLoadProfiles.tsx +++ b/src/qqq/components/misc/SavedBulkLoadProfiles.tsx @@ -59,7 +59,8 @@ interface Props bulkLoadProfileOnChangeCallback?: (record: QRecord | null) => void, allowSelectingProfile?: boolean, fileDescription?: FileDescription, - bulkLoadProfileResetToSuggestedMappingCallback?: () => void + bulkLoadProfileResetToSuggestedMappingCallback?: () => void, + isBulkEdit?: boolean; } SavedBulkLoadProfiles.defaultProps = { @@ -72,7 +73,7 @@ const qController = Client.getInstance(); ** menu-button, text elements, and modal(s) that let you work with saved ** bulk-load profiles. ***************************************************************************/ -function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, currentSavedBulkLoadProfileRecord, bulkLoadProfileOnChangeCallback, currentMapping, allowSelectingProfile, fileDescription, bulkLoadProfileResetToSuggestedMappingCallback}: Props): JSX.Element +function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, currentSavedBulkLoadProfileRecord, bulkLoadProfileOnChangeCallback, currentMapping, allowSelectingProfile, fileDescription, bulkLoadProfileResetToSuggestedMappingCallback, isBulkEdit}: Props): JSX.Element { const [yourSavedBulkLoadProfiles, setYourSavedBulkLoadProfiles] = useState([] as QRecord[]); const [bulkLoadProfilesSharedWithYou, setBulkLoadProfilesSharedWithYou] = useState([] as QRecord[]); @@ -142,6 +143,7 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current const formData = new FormData(); formData.append("tableName", tableMetaData.name); + formData.append("isBulkEdit", isBulkEdit.toString()); const savedBulkLoadProfiles = await makeSavedBulkLoadProfileRequest("querySavedBulkLoadProfile", formData); const yourSavedBulkLoadProfiles: QRecord[] = []; @@ -212,7 +214,7 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current break; case RESET_TO_SUGGESTION: setSavePopupOpen(false); - if(bulkLoadProfileResetToSuggestedMappingCallback) + if (bulkLoadProfileResetToSuggestedMappingCallback) { bulkLoadProfileResetToSuggestedMappingCallback(); } @@ -265,6 +267,7 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current const bulkLoadProfile = currentMapping.toProfile(); const mappingJson = JSON.stringify(bulkLoadProfile.profile); formData.append("mappingJson", mappingJson); + formData.append("isBulkEdit", isBulkEdit.toString()); if (isSaveAsAction || isRenameAction || currentSavedBulkLoadProfileRecord == null) { @@ -389,6 +392,7 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current return (savedBulkLoadProfiles); } + const bulkAction = isBulkEdit ? "Edit" : "Load"; const hasStorePermission = metaData?.processes.has("storeSavedBulkLoadProfile"); const hasDeletePermission = metaData?.processes.has("deleteSavedBulkLoadProfile"); const hasQueryPermission = metaData?.processes.has("querySavedBulkLoadProfile"); @@ -428,15 +432,15 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current PaperProps={{style: {maxHeight: "calc(100vh - 200px)", minWidth: menuWidth}}} > { - Bulk Load Profile Actions + Bulk {bulkAction} Profile Actions } { !allowSelectingProfile && { currentSavedBulkLoadProfileRecord ? - You are using the bulk load profile:
{currentSavedBulkLoadProfileRecord.values.get("label")}

You can manage this profile on this screen.
- : You are not using a saved bulk load profile.

You can save your profile on this screen.
+ You are using the bulk {bulkAction.toLowerCase()} profile:
{currentSavedBulkLoadProfileRecord.values.get("label")}

You can manage this profile on this screen.
+ : You are not using a saved bulk {bulkAction.toLowerCase()} profile.

You can save your profile on this screen.
}
} @@ -456,7 +460,7 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current } { hasStorePermission && currentSavedBulkLoadProfileRecord != null && - + handleDropdownOptionClick(RENAME_OPTION)}> edit @@ -467,7 +471,7 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current } { hasStorePermission && currentSavedBulkLoadProfileRecord != null && - + handleDropdownOptionClick(DUPLICATE_OPTION)}> content_copy @@ -478,7 +482,7 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current } { hasDeletePermission && currentSavedBulkLoadProfileRecord != null && - + handleDropdownOptionClick(DELETE_OPTION)}> delete @@ -489,11 +493,11 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current } { allowSelectingProfile && - + handleDropdownOptionClick(CLEAR_OPTION)}> monitor - New Bulk Load Profile + New Bulk {bulkAction} Profile @@ -504,7 +508,7 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current { } - Your Saved Bulk Load Profiles + Your Saved Bulk {bulkAction} Profiles { yourSavedBulkLoadProfiles && yourSavedBulkLoadProfiles.length > 0 ? ( yourSavedBulkLoadProfiles.map((record: QRecord, index: number) => @@ -514,11 +518,11 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current ) ) : ( - You do not have any saved bulk load profiles for this table. + You do not have any saved bulk {bulkAction.toLowerCase()} profiles for this table. ) } - Bulk Load Profiles Shared with you + Bulk {bulkAction} Profiles Shared with you { bulkLoadProfilesSharedWithYou && bulkLoadProfilesSharedWithYou.length > 0 ? ( bulkLoadProfilesSharedWithYou.map((record: QRecord, index: number) => @@ -528,7 +532,7 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current ) ) : ( - You do not have any bulk load profiles shared with you for this table. + You do not have any bulk {bulkAction.toLowerCase()} profiles shared with you for this table. ) } @@ -537,7 +541,7 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current ); - let buttonText = "Saved Bulk Load Profiles"; + let buttonText = `Saved Bulk ${bulkAction} Profiles`; let buttonBackground = "none"; let buttonBorder = colors.grayLines.main; let buttonColor = colors.gray.main; @@ -639,13 +643,13 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current Unsaved Mapping
    -
  • You are not using a saved bulk load profile.
  • +
  • You are not using a saved bulk {bulkAction.toLowerCase()} profile.
  • { /*bulkLoadProfileDiffs.map((s: string, i: number) =>
  • {s}
  • )*/ }
}> - +
{/* vertical rule */} @@ -716,20 +720,20 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current { currentSavedBulkLoadProfileRecord ? ( isDeleteAction ? ( - Delete Bulk Load Profile + Delete Bulk {bulkAction} Profile ) : ( isSaveAsAction ? ( - Save Bulk Load Profile As + Save Bulk {bulkAction} Profile As ) : ( isRenameAction ? ( - Rename Bulk Load Profile + Rename Bulk {bulkAction} Profile ) : ( - Update Existing Bulk Load Profile + Update Existing Bulk {bulkAction} Profile ) ) ) ) : ( - Save New Bulk Load Profile + Save New Bulk {bulkAction} Profile ) } @@ -743,15 +747,15 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current { isSaveAsAction ? ( - Enter a name for this new saved bulk load profile. + Enter a name for this new saved bulk {bulkAction.toLowerCase()} profile. ) : ( - Enter a new name for this saved bulk load profile. + Enter a new name for this saved bulk {bulkAction.toLowerCase()} profile. ) } ) : ( isDeleteAction ? ( - Are you sure you want to delete the bulk load profile {`'${currentSavedBulkLoadProfileRecord?.values.get("label")}'`}? + Are you sure you want to delete the bulk {bulkAction.toLowerCase()} profile {`'${currentSavedBulkLoadProfileRecord?.values.get("label")}'`}? ) : ( - Are you sure you want to update the bulk load profile {`'${currentSavedBulkLoadProfileRecord?.values.get("label")}'`}? + Are you sure you want to update the bulk {bulkAction.toLowerCase()} profile {`'${currentSavedBulkLoadProfileRecord?.values.get("label")}'`}? ) ) } diff --git a/src/qqq/components/processes/BulkLoadFileMappingField.tsx b/src/qqq/components/processes/BulkLoadFileMappingField.tsx index 3023f0b..085cde0 100644 --- a/src/qqq/components/processes/BulkLoadFileMappingField.tsx +++ b/src/qqq/components/processes/BulkLoadFileMappingField.tsx @@ -43,6 +43,7 @@ interface BulkLoadMappingFieldProps removeFieldCallback?: () => void, fileDescription: FileDescription, forceParentUpdate?: () => void, + isBulkEdit?: boolean } const xIconButtonSX = @@ -71,7 +72,7 @@ const qController = Client.getInstance(); /*************************************************************************** ** row for a single field on the bulk load mapping screen. ***************************************************************************/ -export default function BulkLoadFileMappingField({bulkLoadField, isRequired, removeFieldCallback, fileDescription, forceParentUpdate}: BulkLoadMappingFieldProps): JSX.Element +export default function BulkLoadFileMappingField({bulkLoadField, isRequired, removeFieldCallback, fileDescription, forceParentUpdate, isBulkEdit}: BulkLoadMappingFieldProps): JSX.Element { const columnNames = fileDescription.getColumnNames(); @@ -227,6 +228,17 @@ export default function BulkLoadFileMappingField({bulkLoadField, isRequired, rem forceParentUpdate && forceParentUpdate(); } + + /*************************************************************************** + ** + ***************************************************************************/ + function clearIfEmptyChanged(value: boolean) + { + bulkLoadField.clearIfEmpty = value; + forceParentUpdate && forceParentUpdate(); + } + + /*************************************************************************** ** ***************************************************************************/ @@ -313,8 +325,11 @@ export default function BulkLoadFileMappingField({bulkLoadField, isRequired, rem { valueType == "column" && <> - + mapValuesChanged(checked)} />} label={"Map values"} sx={{minWidth: "140px", whiteSpace: "nowrap"}} /> + { + isBulkEdit && !isRequired && clearIfEmptyChanged(checked)} />} label={"Clear if empty"} sx={{minWidth: "140px", whiteSpace: "nowrap"}} /> + } Preview Values: {(fileDescription.getPreviewValues(selectedColumn?.value) ?? [""]).join(", ")} diff --git a/src/qqq/components/processes/BulkLoadFileMappingFields.tsx b/src/qqq/components/processes/BulkLoadFileMappingFields.tsx index 0aa74cf..dec7302 100644 --- a/src/qqq/components/processes/BulkLoadFileMappingFields.tsx +++ b/src/qqq/components/processes/BulkLoadFileMappingFields.tsx @@ -33,6 +33,7 @@ interface BulkLoadMappingFieldsProps bulkLoadMapping: BulkLoadMapping, fileDescription: FileDescription, forceParentUpdate?: () => void, + isBulkEdit?: boolean } @@ -43,7 +44,7 @@ const ALREADY_ADDED_FIELD_TOOLTIP = "This field has already been added to your m /*************************************************************************** ** The section of the bulk load mapping screen with all the fields. ***************************************************************************/ -export default function BulkLoadFileMappingFields({bulkLoadMapping, fileDescription, forceParentUpdate}: BulkLoadMappingFieldsProps): JSX.Element +export default function BulkLoadFileMappingFields({bulkLoadMapping, fileDescription, forceParentUpdate, isBulkEdit}: BulkLoadMappingFieldsProps): JSX.Element { const [, forceUpdate] = useReducer((x) => x + 1, 0); @@ -254,11 +255,16 @@ export default function BulkLoadFileMappingFields({bulkLoadMapping, fileDescript return ( <> -
Required Fields
+ {isBulkEdit ?
Key Fields
:
Required Fields
} { bulkLoadMapping.requiredFields.length == 0 && - There are no required fields in this table. + ( + isBulkEdit ? + Select table key fields to continue. + : + There are no required fields in this table. + ) } {bulkLoadMapping.requiredFields.map((bulkLoadField) => ( ))} -
Additional Fields
+ {isBulkEdit ?
Fields To Update
:
Additional Fields
} {bulkLoadMapping.additionalFields.map((bulkLoadField) => ( removeField(bulkLoadField)} forceParentUpdate={forceParentUpdate} + isBulkEdit={isBulkEdit} /> ))} diff --git a/src/qqq/components/processes/BulkLoadFileMappingForm.tsx b/src/qqq/components/processes/BulkLoadFileMappingForm.tsx index 6a6759a..aaf74dc 100644 --- a/src/qqq/components/processes/BulkLoadFileMappingForm.tsx +++ b/src/qqq/components/processes/BulkLoadFileMappingForm.tsx @@ -36,15 +36,18 @@ import {useFormikContext} from "formik"; import colors from "qqq/assets/theme/base/colors"; import {DynamicFormFieldLabel} from "qqq/components/forms/DynamicForm"; import QDynamicFormField from "qqq/components/forms/DynamicFormField"; +import DynamicFormUtils from "qqq/components/forms/DynamicFormUtils"; import MDTypography from "qqq/components/legacy/MDTypography"; import HelpContent from "qqq/components/misc/HelpContent"; 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, useImperativeHandle, useReducer, useState} from "react"; +import Client from "qqq/utils/qqq/Client"; +import React, {forwardRef, useEffect, useImperativeHandle, useReducer, useState} from "react"; import ProcessViewForm from "./ProcessViewForm"; +const qController = Client.getInstance(); interface BulkLoadMappingFormProps { @@ -73,13 +76,12 @@ const BulkLoadFileMappingForm = forwardRef(({processValues, tableMetaData, metaD const [suggestedBulkLoadProfile] = useState(processValues.suggestedBulkLoadProfile as BulkLoadProfile); const [tableStructure] = useState(processValues.tableStructure as BulkLoadTableStructure); - const [bulkLoadMapping, setBulkLoadMapping] = useState(BulkLoadMapping.fromBulkLoadProfile(tableStructure, processValues.bulkLoadProfile)); + const [bulkLoadMapping, setBulkLoadMapping] = useState(BulkLoadMapping.fromBulkLoadProfile(tableStructure, processValues.bulkLoadProfile, processMetaData.name)); const [wrappedBulkLoadMapping] = useState(new Wrapper(bulkLoadMapping)); const [fileDescription] = useState(new FileDescription(processValues.headerValues, processValues.headerLetters, processValues.bodyValuesPreview)); fileDescription.setHasHeaderRow(bulkLoadMapping.hasHeaderRow); - const [, forceUpdate] = useReducer((x) => x + 1, 0); ///////////////////////////////////////////////////////////////////////////////////////////////// @@ -114,6 +116,8 @@ const BulkLoadFileMappingForm = forwardRef(({processValues, tableMetaData, metaD values["savedBulkLoadProfileId"] = wrappedCurrentSavedBulkLoadProfile.get()?.values?.get("id"); values["layout"] = wrappedBulkLoadMapping.get().layout; values["hasHeaderRow"] = wrappedBulkLoadMapping.get().hasHeaderRow; + values["isBulkEdit"] = wrappedBulkLoadMapping.get().isBulkEdit; + values["keyFields"] = wrappedBulkLoadMapping.get().keyFields; let haveLocalErrors = false; const fieldErrors: { [fieldName: string]: string } = {}; @@ -130,7 +134,14 @@ const BulkLoadFileMappingForm = forwardRef(({processValues, tableMetaData, metaD } setFieldErrors(fieldErrors); - if(wrappedBulkLoadMapping.get().requiredFields.length == 0 && wrappedBulkLoadMapping.get().additionalFields.length == 0) + if (values["isBulkEdit"] && (values["keyFields"] == null || values["keyFields"] == undefined)) + { + haveLocalErrors = true; + fieldErrors["keyFields"] = "This field is required."; + } + setFieldErrors(fieldErrors); + + if (wrappedBulkLoadMapping.get().requiredFields.length == 0 && wrappedBulkLoadMapping.get().additionalFields.length == 0) { setNoMappedFieldsError("You must have at least 1 field."); haveLocalErrors = true; @@ -141,9 +152,9 @@ const BulkLoadFileMappingForm = forwardRef(({processValues, tableMetaData, metaD setNoMappedFieldsError(null); } - if(haveProfileErrors) + if (haveProfileErrors) { - setTimeout(() => + setTimeout(() => { document.querySelector(".bulkLoadFieldError")?.scrollIntoView({behavior: "smooth", block: "center", inline: "center"}); }, 250); @@ -182,7 +193,7 @@ const BulkLoadFileMappingForm = forwardRef(({processValues, tableMetaData, metaD ***************************************************************************/ function bulkLoadProfileResetToSuggestedMappingCallback() { - handleNewBulkLoadMapping(BulkLoadMapping.fromBulkLoadProfile(processValues.tableStructure, suggestedBulkLoadProfile)); + handleNewBulkLoadMapping(BulkLoadMapping.fromBulkLoadProfile(processValues.tableStructure, suggestedBulkLoadProfile, processValues.name)); } @@ -201,6 +212,8 @@ const BulkLoadFileMappingForm = forwardRef(({processValues, tableMetaData, metaD setBulkLoadMapping(newBulkLoadMapping); wrappedBulkLoadMapping.set(newBulkLoadMapping); + setFieldValue("isBulkEdit", newBulkLoadMapping.isBulkEdit); + setFieldValue("keyFields", newBulkLoadMapping.keyFields); setFieldValue("hasHeaderRow", newBulkLoadMapping.hasHeaderRow); setFieldValue("layout", newBulkLoadMapping.layout); @@ -228,10 +241,13 @@ const BulkLoadFileMappingForm = forwardRef(({processValues, tableMetaData, metaD bulkLoadProfileOnChangeCallback={bulkLoadProfileOnChangeCallback} bulkLoadProfileResetToSuggestedMappingCallback={bulkLoadProfileResetToSuggestedMappingCallback} fileDescription={fileDescription} + isBulkEdit={processValues.isBulkEdit} /> @@ -267,6 +284,7 @@ export default BulkLoadFileMappingForm; interface BulkLoadMappingHeaderProps { + isBulkEdit?: boolean, fileDescription: FileDescription, fileName: string, bulkLoadMapping?: BulkLoadMapping, @@ -275,13 +293,16 @@ interface BulkLoadMappingHeaderProps forceParentUpdate?: () => void, frontendStep: QFrontendStepMetaData, processMetaData: QProcessMetaData, + tableMetaData: QTableMetaData, } /*************************************************************************** ** private subcomponent - the header section of the bulk load file mapping screen. ***************************************************************************/ -function BulkLoadMappingHeader({fileDescription, fileName, bulkLoadMapping, fieldErrors, tableStructure, forceParentUpdate, frontendStep, processMetaData}: BulkLoadMappingHeaderProps): JSX.Element +function BulkLoadMappingHeader({isBulkEdit, fileDescription, fileName, bulkLoadMapping, fieldErrors, tableStructure, forceParentUpdate, frontendStep, processMetaData, tableMetaData}: BulkLoadMappingHeaderProps): JSX.Element { + const [dynamicField, setDynamicField] = useState(null); + const viewFields = [ new QFieldMetaData({name: "fileName", label: "File Name", type: "STRING"}), new QFieldMetaData({name: "fileDetails", label: "File Details", type: "STRING"}), @@ -307,6 +328,36 @@ function BulkLoadMappingHeader({fileDescription, fileName, bulkLoadMapping, fiel const selectedLayout = layoutOptions.filter(o => o.id == bulkLoadMapping.layout)[0] ?? null; + useEffect(() => + { + (async () => + { + if (isBulkEdit) + { + ///////////////////////////////////////////////////////////////////////// + // if doing a bulk edit, the selected keyFields and set as the display // + ///////////////////////////////////////////////////////////////////////// + const displayValues = new Map; + if (bulkLoadMapping.keyFields) + { + const possibleValues = await qController.possibleValues(null, processMetaData.name, "tableKeyFields", bulkLoadMapping.keyFields, null); + console.log("Received possible values of: " + JSON.stringify(possibleValues)); + displayValues.set("tableKeyFields", possibleValues[0].label); + } + + const tableKeyFieldsField = processMetaData.frontendSteps.find(s => s.name == "fileMapping")?.formFields.find(f => f.name == "tableKeyFields"); + const newDynamicField = DynamicFormUtils.getDynamicField(tableKeyFieldsField); + const dynamicFieldInObject: any = {}; + dynamicFieldInObject[tableKeyFieldsField["name"]] = newDynamicField; + DynamicFormUtils.addPossibleValueProps(dynamicFieldInObject, [tableKeyFieldsField], null, processMetaData.name, displayValues); + + keyFieldsChanged(bulkLoadMapping.keyFields); + setDynamicField(newDynamicField); + forceParentUpdate(); + } + })(); + }, [JSON.stringify(bulkLoadMapping)]); + /*************************************************************************** ** ***************************************************************************/ @@ -331,6 +382,61 @@ function BulkLoadMappingHeader({fileDescription, fileName, bulkLoadMapping, fiel forceParentUpdate(); } + /*************************************************************************** + ** + ***************************************************************************/ + async function keyFieldsChanged(newValue: any) + { + fieldErrors.keyFields = null; + + if (newValue && newValue.length > 0) + { + ////////////////////////////////////////////////////////// + // validate that the fields in the key have been mapped // + ////////////////////////////////////////////////////////// + console.log("Received key fields of: " + newValue); + const keyFields = newValue.split("|"); + const unmappedKeyFields: string[] = []; + const requiredFields: BulkLoadField[] = []; + const additionalFields: BulkLoadField[] = []; + + //////////////////////////////////////////////////////////////////////////////////////////////// + // iterate over all fields in the table, when there are key fields found, make them required, // + // otherwise add them to addition fields // + //////////////////////////////////////////////////////////////////////////////////////////////// + for (let bulkLoadField of [...bulkLoadMapping.requiredFields, ...bulkLoadMapping.additionalFields]) + { + const qualifiedName = bulkLoadField.getQualifiedName(); + const keyField = keyFields.find((k: string) => k == qualifiedName); + if (keyField) + { + requiredFields.push(bulkLoadField); + var fieldsByTablePrefix = bulkLoadMapping.fieldsByTablePrefix[""][keyField]; + if (!fieldsByTablePrefix || fieldsByTablePrefix.columnIndex == null) + { + unmappedKeyFields.push(tableMetaData.fields.get(keyField).label); + } + } + else + { + additionalFields.push(bulkLoadField); + } + } + + bulkLoadMapping.requiredFields = requiredFields; + bulkLoadMapping.additionalFields = additionalFields; + + if (unmappedKeyFields.length > 0) + { + fieldErrors.keyFields = "The following key fields are not mapped: " + unmappedKeyFields.join(", "); + } + + bulkLoadMapping.handleChangeToKeyFields(newValue); + } + + forceParentUpdate(); + } + /*************************************************************************** ** ***************************************************************************/ @@ -369,27 +475,50 @@ function BulkLoadMappingHeader({fileDescription, fileName, bulkLoadMapping, fiel {getFormattedHelpContent("hasHeaderRow")} - - ()} - 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) => (
  • {option?.label ?? ""}
  • )} - disableClearable - sx={{"& .MuiOutlinedInput-root": {padding: "0"}}} - /> { - fieldErrors.layout && - - {
    {fieldErrors.layout}
    } -
    + !isBulkEdit ? ( + <> + + ()} + 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) => (
  • {option?.label ?? ""}
  • )} + disableClearable + sx={{"& .MuiOutlinedInput-root": {padding: "0"}}} + /> + { + fieldErrors.layout && + + {
    {fieldErrors.layout}
    } +
    + } + {getFormattedHelpContent("layout")} + + ) : ( + <> + { + dynamicField && + <> + + + { + fieldErrors.keyFields && + + {
    {fieldErrors.keyFields}
    } +
    + } + {getFormattedHelpContent("tableKeyFields")} + + } + + ) } - {getFormattedHelpContent("layout")}
    @@ -490,25 +619,25 @@ function BulkLoadMappingFilePreview({fileDescription, bulkLoadMapping}: BulkLoad const fields = bulkLoadMapping.getFieldsForColumnIndex(index); const count = fields.length; - let dupeWarning = <> - if(fileDescription.hasHeaderRow && fileDescription.duplicateHeaderIndexes[index]) + let dupeWarning = <>; + if (fileDescription.hasHeaderRow && fileDescription.duplicateHeaderIndexes[index]) { dupeWarning = warning - +
    ; } return ( <> { count > 0 && - - - {dupeWarning} - {letter} - - - + + + {dupeWarning} + {letter} + + + } { count == 0 && {dupeWarning}{letter} @@ -528,24 +657,24 @@ function BulkLoadMappingFilePreview({fileDescription, bulkLoadMapping}: BulkLoad const count = fields.length; const tdStyle = {color: getHeaderColor(count), cursor: getCursor(count), backgroundColor: ""}; - if(fileDescription.hasHeaderRow) + if (fileDescription.hasHeaderRow) { tdStyle.backgroundColor = "#ebebeb"; - if(count > 0) + if (count > 0) { return {value} - + ; } else { - return {value} + return {value}; } } else { - return {value} + return {value}; } } )} diff --git a/src/qqq/components/processes/BulkLoadProfileForm.tsx b/src/qqq/components/processes/BulkLoadProfileForm.tsx index f1e5403..3a67c19 100644 --- a/src/qqq/components/processes/BulkLoadProfileForm.tsx +++ b/src/qqq/components/processes/BulkLoadProfileForm.tsx @@ -43,12 +43,12 @@ interface BulkLoadValueMappingFormProps const BulkLoadProfileForm = forwardRef(({processValues, tableMetaData, metaData}: BulkLoadValueMappingFormProps, ref) => { const savedBulkLoadProfileRecordProcessValue = processValues.savedBulkLoadProfileRecord; - const [savedBulkLoadProfileRecord, setSavedBulkLoadProfileRecord] = useState(savedBulkLoadProfileRecordProcessValue == null ? null : new QRecord(savedBulkLoadProfileRecordProcessValue)) + 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 [currentMapping, setCurrentMapping] = useState(BulkLoadMapping.fromBulkLoadProfile(tableStructure, bulkLoadProfile)); const [wrappedCurrentSavedBulkLoadProfile] = useState(new Wrapper(savedBulkLoadProfileRecord)); const [fileDescription] = useState(new FileDescription(processValues.headerValues, processValues.headerLetters, processValues.bodyValuesPreview)); @@ -93,10 +93,11 @@ const BulkLoadProfileForm = forwardRef(({processValues, tableMetaData, metaData} allowSelectingProfile={false} fileDescription={fileDescription} bulkLoadProfileOnChangeCallback={bulkLoadProfileOnChangeCallback} + isBulkEdit={processValues.isBulkEdit} /> ); }); -export default BulkLoadProfileForm; \ No newline at end of file +export default BulkLoadProfileForm; diff --git a/src/qqq/components/processes/BulkLoadValueMappingForm.tsx b/src/qqq/components/processes/BulkLoadValueMappingForm.tsx index b785c25..778fd18 100644 --- a/src/qqq/components/processes/BulkLoadValueMappingForm.tsx +++ b/src/qqq/components/processes/BulkLoadValueMappingForm.tsx @@ -75,7 +75,7 @@ const BulkLoadValueMappingForm = forwardRef(({processValues, setActiveStepLabel, *******************************************************************************/ function initializeCurrentBulkLoadMapping(): BulkLoadMapping { - const bulkLoadMapping = BulkLoadMapping.fromBulkLoadProfile(tableStructure, bulkLoadProfile); + const bulkLoadMapping = BulkLoadMapping.fromBulkLoadProfile(tableStructure, bulkLoadProfile, processValues.name); if (!bulkLoadMapping.valueMappings[fieldFullName]) { @@ -155,7 +155,7 @@ const BulkLoadValueMappingForm = forwardRef(({processValues, setActiveStepLabel, function mappedValueChanged(fileValue: string, newValue: any) { valueErrors[fileValue] = null; - if(newValue == null) + if (newValue == null) { delete currentMapping.valueMappings[fieldFullName][fileValue]; } @@ -195,6 +195,7 @@ const BulkLoadValueMappingForm = forwardRef(({processValues, setActiveStepLabel, allowSelectingProfile={false} bulkLoadProfileOnChangeCallback={bulkLoadProfileOnChangeCallback} fileDescription={fileDescription} + isBulkEdit={processValues.isBulkEdit} /> diff --git a/src/qqq/components/query/QueryScreenActionMenu.tsx b/src/qqq/components/query/QueryScreenActionMenu.tsx index 73147a4..8e7c2db 100644 --- a/src/qqq/components/query/QueryScreenActionMenu.tsx +++ b/src/qqq/components/query/QueryScreenActionMenu.tsx @@ -29,9 +29,9 @@ import Icon from "@mui/material/Icon"; import ListItemIcon from "@mui/material/ListItemIcon"; import Menu from "@mui/material/Menu"; import MenuItem from "@mui/material/MenuItem"; +import {QActionsMenuButton} from "qqq/components/buttons/DefaultButtons"; import React, {useState} from "react"; import {useNavigate} from "react-router-dom"; -import {QActionsMenuButton} from "qqq/components/buttons/DefaultButtons"; interface QueryScreenActionMenuProps { @@ -40,28 +40,28 @@ interface QueryScreenActionMenuProps tableProcesses: QProcessMetaData[]; bulkLoadClicked: () => void; bulkEditClicked: () => void; + bulkEditWithFileClicked: () => void; bulkDeleteClicked: () => void; processClicked: (process: QProcessMetaData) => void; } -QueryScreenActionMenu.defaultProps = { -}; +QueryScreenActionMenu.defaultProps = {}; -export default function QueryScreenActionMenu({metaData, tableMetaData, tableProcesses, bulkLoadClicked, bulkEditClicked, bulkDeleteClicked, processClicked}: QueryScreenActionMenuProps): JSX.Element +export default function QueryScreenActionMenu({metaData, tableMetaData, tableProcesses, bulkLoadClicked, bulkEditClicked, bulkEditWithFileClicked, bulkDeleteClicked, processClicked}: QueryScreenActionMenuProps): JSX.Element { - const [anchorElement, setAnchorElement] = useState(null) + const [anchorElement, setAnchorElement] = useState(null); const navigate = useNavigate(); const openActionsMenu = (event: any) => { setAnchorElement(event.currentTarget); - } + }; const closeActionsMenu = () => { setAnchorElement(null); - } + }; const pushDividerIfNeeded = (menuItems: JSX.Element[]) => { @@ -75,7 +75,7 @@ export default function QueryScreenActionMenu({metaData, tableMetaData, tablePro { closeActionsMenu(); handler(); - } + }; const menuItems: JSX.Element[] = []; if (tableMetaData.capabilities.has(Capability.TABLE_INSERT) && tableMetaData.insertPermission) @@ -85,6 +85,7 @@ export default function QueryScreenActionMenu({metaData, tableMetaData, tablePro if (tableMetaData.capabilities.has(Capability.TABLE_UPDATE) && tableMetaData.editPermission) { menuItems.push( runSomething(bulkEditClicked)}>editBulk Edit); + menuItems.push( runSomething(bulkEditWithFileClicked)}>edit_noteBulk Edit With File); } if (tableMetaData.capabilities.has(Capability.TABLE_DELETE) && tableMetaData.deletePermission) { @@ -130,5 +131,5 @@ export default function QueryScreenActionMenu({metaData, tableMetaData, tablePro {menuItems} - ) + ); } diff --git a/src/qqq/models/processes/BulkLoadModels.ts b/src/qqq/models/processes/BulkLoadModels.ts index 955e355..22dae95 100644 --- a/src/qqq/models/processes/BulkLoadModels.ts +++ b/src/qqq/models/processes/BulkLoadModels.ts @@ -39,6 +39,7 @@ export class BulkLoadField headerName?: string = null; defaultValue?: any = null; doValueMapping: boolean = false; + clearIfEmpty?: boolean = false; wideLayoutIndexPath: number[] = []; @@ -51,7 +52,7 @@ export class BulkLoadField /*************************************************************************** ** ***************************************************************************/ - constructor(field: QFieldMetaData, tableStructure: BulkLoadTableStructure, valueType: ValueType = "column", columnIndex?: number, headerName?: string, defaultValue?: any, doValueMapping?: boolean, wideLayoutIndexPath: number[] = [], error: string = null, warning: string = null) + constructor(field: QFieldMetaData, tableStructure: BulkLoadTableStructure, valueType: ValueType = "column", columnIndex?: number, headerName?: string, defaultValue?: any, doValueMapping?: boolean, wideLayoutIndexPath: number[] = [], error: string = null, warning: string = null, clearIfEmpty?: boolean) { this.field = field; this.tableStructure = tableStructure; @@ -64,6 +65,7 @@ export class BulkLoadField this.error = error; this.warning = warning; this.key = new Date().getTime().toString(); + this.clearIfEmpty = clearIfEmpty ?? false; } @@ -72,7 +74,7 @@ export class BulkLoadField ***************************************************************************/ public static clone(source: BulkLoadField): BulkLoadField { - return (new BulkLoadField(source.field, source.tableStructure, source.valueType, source.columnIndex, source.headerName, source.defaultValue, source.doValueMapping, source.wideLayoutIndexPath, source.error, source.warning)); + return (new BulkLoadField(source.field, source.tableStructure, source.valueType, source.columnIndex, source.headerName, source.defaultValue, source.doValueMapping, source.wideLayoutIndexPath, source.error, source.warning, source.clearIfEmpty)); } @@ -173,6 +175,9 @@ export interface BulkLoadTableStructure associationPath: string; fields: QFieldMetaData[]; associations: BulkLoadTableStructure[]; + isBulkEdit: boolean; + possibleKeyFields: string[]; + keyFields?: string; } @@ -193,6 +198,8 @@ export class BulkLoadMapping valueMappings: { [fieldName: string]: { [fileValue: string]: any } } = {}; + isBulkEdit: boolean; + keyFields: string; hasHeaderRow: boolean; layout: string; @@ -211,6 +218,8 @@ export class BulkLoadMapping } } + this.isBulkEdit = tableStructure.isBulkEdit; + this.keyFields = tableStructure.keyFields; this.hasHeaderRow = true; } @@ -218,11 +227,13 @@ export class BulkLoadMapping /*************************************************************************** ** ***************************************************************************/ - private processTableStructure(tableStructure: BulkLoadTableStructure) + public processTableStructure(tableStructure: BulkLoadTableStructure) { const prefix = tableStructure.isMain ? "" : tableStructure.associationPath; this.fieldsByTablePrefix[prefix] = {}; this.tablesByPath[prefix] = tableStructure; + this.isBulkEdit = tableStructure.isBulkEdit; + this.keyFields = tableStructure.keyFields; for (let field of tableStructure.fields) { @@ -233,13 +244,35 @@ export class BulkLoadMapping this.fields[qualifiedName] = bulkLoadField; this.fieldsByTablePrefix[prefix][qualifiedName] = bulkLoadField; - if (tableStructure.isMain && field.isRequired) + if (this.isBulkEdit) { - this.requiredFields.push(bulkLoadField); + if (this.keyFields == null) + { + this.unusedFields.push(bulkLoadField); + } + else + { + const keyFields = this.keyFields.split("|"); + if (keyFields.includes(qualifiedName)) + { + this.requiredFields.push(bulkLoadField); + } + else + { + this.unusedFields.push(bulkLoadField); + } + } } else { - this.unusedFields.push(bulkLoadField); + if (tableStructure.isMain && field.isRequired) + { + this.requiredFields.push(bulkLoadField); + } + else + { + this.unusedFields.push(bulkLoadField); + } } } } @@ -266,14 +299,16 @@ export class BulkLoadMapping ** take a saved bulk load profile - and convert it into a working bulkLoadMapping ** for the frontend to use! ***************************************************************************/ - public static fromBulkLoadProfile(tableStructure: BulkLoadTableStructure, bulkLoadProfile: BulkLoadProfile): BulkLoadMapping + public static fromBulkLoadProfile(tableStructure: BulkLoadTableStructure, bulkLoadProfile: BulkLoadProfile, processName?: string): BulkLoadMapping { const bulkLoadMapping = new BulkLoadMapping(tableStructure); if (bulkLoadProfile.version == "v1") { + bulkLoadMapping.isBulkEdit = bulkLoadProfile.isBulkEdit; bulkLoadMapping.hasHeaderRow = bulkLoadProfile.hasHeaderRow; bulkLoadMapping.layout = bulkLoadProfile.layout; + bulkLoadMapping.keyFields = bulkLoadProfile.keyFields; //////////////////////////////////////////////////////////////////////////////////////////////////////////////// // function to get a bulkLoadMapping field by its (full) name - whether that's in the required fields list, // @@ -322,6 +357,7 @@ export class BulkLoadMapping { bulkLoadField.valueType = "column"; bulkLoadField.doValueMapping = bulkLoadProfileField.doValueMapping; + bulkLoadField.clearIfEmpty = bulkLoadProfileField.clearIfEmpty; bulkLoadField.headerName = bulkLoadProfileField.headerName; bulkLoadField.columnIndex = bulkLoadProfileField.columnIndex; @@ -344,6 +380,29 @@ export class BulkLoadMapping } } + if (!bulkLoadMapping.keyFields && tableStructure.possibleKeyFields?.length > 0) + { + //////////////////////////////////////////////////////////////////////////////////////////////// + // look at each of the possible key fields, compare with the fields in the bulk load profile, // + // on the first one that matches, use that as the default bulk load mapping key field // + //////////////////////////////////////////////////////////////////////////////////////////////// + for (let keyField of tableStructure.possibleKeyFields) + { + const parts = keyField.split("|"); + const allPartsMatch = parts.every(part => + (bulkLoadProfile.fieldList ?? []).some((field: BulkLoadProfileField) => + field.fieldName === part + ) + ); + + if (allPartsMatch) + { + bulkLoadMapping.keyFields = keyField; + break; // stop after the first valid match + } + } + } + return (bulkLoadMapping); } else @@ -365,6 +424,8 @@ export class BulkLoadMapping profile.version = "v1"; profile.hasHeaderRow = this.hasHeaderRow; profile.layout = this.layout; + profile.isBulkEdit = this.isBulkEdit; + profile.keyFields = this.keyFields; for (let bulkLoadField of [...this.requiredFields, ...this.additionalFields]) { @@ -384,7 +445,7 @@ export class BulkLoadMapping } else { - const field: BulkLoadProfileField = {fieldName: fullFieldName, columnIndex: bulkLoadField.columnIndex, headerName: bulkLoadField.headerName, doValueMapping: bulkLoadField.doValueMapping}; + const field: BulkLoadProfileField = {fieldName: fullFieldName, columnIndex: bulkLoadField.columnIndex, headerName: bulkLoadField.headerName, doValueMapping: bulkLoadField.doValueMapping, clearIfEmpty: bulkLoadField.clearIfEmpty}; if (this.valueMappings[fullFieldName]) { @@ -576,6 +637,16 @@ export class BulkLoadMapping return (rs); } + + /*************************************************************************** + ** + ***************************************************************************/ + public handleChangeToKeyFields(newKeyFields: any) + { + this.keyFields = newKeyFields; + } + + /*************************************************************************** ** ***************************************************************************/ @@ -600,7 +671,7 @@ export class BulkLoadMapping { const newField = BulkLoadField.clone(field); newField.columnIndex = null; - newField.warning = "This field was assigned to a column with a duplicated header" + newField.warning = "This field was assigned to a column with a duplicated header"; newRequiredFields.push(newField); anyChangesToRequiredFields = true; } @@ -616,7 +687,7 @@ export class BulkLoadMapping { const newField = BulkLoadField.clone(field); newField.columnIndex = null; - newField.warning = "This field was assigned to a column with a duplicated header" + newField.warning = "This field was assigned to a column with a duplicated header"; newAdditionalFields.push(newField); anyChangesToAdditionalFields = true; } @@ -798,6 +869,8 @@ export class BulkLoadProfile fieldList: BulkLoadProfileField[] = []; hasHeaderRow: boolean; layout: string; + isBulkEdit: boolean; + keyFields: string; } type BulkLoadProfileField = @@ -807,6 +880,7 @@ type BulkLoadProfileField = headerName?: string, defaultValue?: any, doValueMapping?: boolean, + clearIfEmpty?: boolean, valueMappings?: { [fileValue: string]: any } }; diff --git a/src/qqq/pages/records/query/RecordQuery.tsx b/src/qqq/pages/records/query/RecordQuery.tsx index b84a714..64a4013 100644 --- a/src/qqq/pages/records/query/RecordQuery.tsx +++ b/src/qqq/pages/records/query/RecordQuery.tsx @@ -1557,7 +1557,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable /******************************************************************************* ** function to open one of the bulk (insert/edit/delete) processes. *******************************************************************************/ - const openBulkProcess = (processNamePart: "Insert" | "Edit" | "Delete", processLabelPart: "Load" | "Edit" | "Delete") => + const openBulkProcess = (processNamePart: "Insert" | "Edit" | "Delete" | "EditWithFile", processLabelPart: "Load" | "Edit" | "Delete" | "Edit With File") => { const processList = allTableProcesses.filter(p => p.name.endsWith(`.bulk${processNamePart}`)); if (processList.length > 0) @@ -1593,6 +1593,15 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable }; + /******************************************************************************* + ** Event handler for the bulk-edit-with-file process being selected + *******************************************************************************/ + const bulkEditWithFileClicked = () => + { + openBulkProcess("EditWithFile", "Edit With File"); + }; + + /******************************************************************************* ** Event handler for the bulk-delete process being selected *******************************************************************************/ @@ -2861,6 +2870,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable tableProcesses={tableProcesses} bulkLoadClicked={bulkLoadClicked} bulkEditClicked={bulkEditClicked} + bulkEditWithFileClicked={bulkEditWithFileClicked} bulkDeleteClicked={bulkDeleteClicked} processClicked={processClicked} />