/* * QQQ - Low-code Application Framework for Engineers. * Copyright (C) 2021-2022. Kingsrook, LLC * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States * contact@kingsrook.com * https://github.com/Kingsrook/ * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ import {QController} from "@kingsrook/qqq-frontend-core/lib/controllers/QController"; import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance"; import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData"; import {QJobComplete} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobComplete"; import {QJobError} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobError"; import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord"; import {Alert, Button} from "@mui/material"; import Box from "@mui/material/Box"; import Dialog from "@mui/material/Dialog"; import DialogActions from "@mui/material/DialogActions"; import DialogContent from "@mui/material/DialogContent"; import DialogTitle from "@mui/material/DialogTitle"; import Divider from "@mui/material/Divider"; 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 TextField from "@mui/material/TextField"; import Tooltip from "@mui/material/Tooltip"; import {TooltipProps} from "@mui/material/Tooltip/Tooltip"; import FormData from "form-data"; import QContext from "QContext"; import colors from "qqq/assets/theme/base/colors"; import {QCancelButton, QDeleteButton, QSaveButton} from "qqq/components/buttons/DefaultButtons"; import {BulkLoadMapping, BulkLoadTableStructure, FileDescription} from "qqq/models/processes/BulkLoadModels"; import Client from "qqq/utils/qqq/Client"; import {SavedBulkLoadProfileUtils} from "qqq/utils/qqq/SavedBulkLoadProfileUtils"; import React, {useContext, useEffect, useRef, useState} from "react"; import {useLocation} from "react-router-dom"; interface Props { metaData: QInstance, tableMetaData: QTableMetaData, tableStructure: BulkLoadTableStructure, currentSavedBulkLoadProfileRecord: QRecord, currentMapping: BulkLoadMapping, bulkLoadProfileOnChangeCallback?: (record: QRecord | null) => void, allowSelectingProfile?: boolean, fileDescription?: FileDescription, bulkLoadProfileResetToSuggestedMappingCallback?: () => void } SavedBulkLoadProfiles.defaultProps = { allowSelectingProfile: true }; 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 { const [yourSavedBulkLoadProfiles, setYourSavedBulkLoadProfiles] = useState([] as QRecord[]); const [bulkLoadProfilesSharedWithYou, setBulkLoadProfilesSharedWithYou] = useState([] as QRecord[]); const [savedBulkLoadProfilesMenu, setSavedBulkLoadProfilesMenu] = useState(null); const [savedBulkLoadProfilesHaveLoaded, setSavedBulkLoadProfilesHaveLoaded] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const [savePopupOpen, setSavePopupOpen] = useState(false); const [isSaveAsAction, setIsSaveAsAction] = useState(false); const [isRenameAction, setIsRenameAction] = useState(false); const [isDeleteAction, setIsDeleteAction] = useState(false); const [savedBulkLoadProfileNameInputValue, setSavedBulkLoadProfileNameInputValue] = useState(null as string); const [popupAlertContent, setPopupAlertContent] = useState(""); const [savedSuccessMessage, setSavedSuccessMessage] = useState(null as string); const [savedFailedMessage, setSavedFailedMessage] = useState(null as string); const anchorRef = useRef(null); const location = useLocation(); const [saveOptionsOpen, setSaveOptionsOpen] = useState(false); const SAVE_OPTION = "Save..."; const DUPLICATE_OPTION = "Duplicate..."; const RENAME_OPTION = "Rename..."; const DELETE_OPTION = "Delete..."; const CLEAR_OPTION = "New Profile"; const RESET_TO_SUGGESTION = "Reset to Suggested Mapping"; const {accentColor, accentColorLight, userId: currentUserId} = useContext(QContext); const openSavedBulkLoadProfilesMenu = (event: any) => setSavedBulkLoadProfilesMenu(event.currentTarget); const closeSavedBulkLoadProfilesMenu = () => setSavedBulkLoadProfilesMenu(null); //////////////////////////////////////////////////////////////////////// // load records on first run (if user is allowed to select a profile) // //////////////////////////////////////////////////////////////////////// useEffect(() => { if (allowSelectingProfile) { loadSavedBulkLoadProfiles() .then(() => { setSavedBulkLoadProfilesHaveLoaded(true); }); } }, []); const baseBulkLoadMapping: BulkLoadMapping = currentSavedBulkLoadProfileRecord ? BulkLoadMapping.fromSavedProfileRecord(tableStructure, currentSavedBulkLoadProfileRecord) : new BulkLoadMapping(tableStructure); const bulkLoadProfileDiffs: any[] = SavedBulkLoadProfileUtils.diffBulkLoadMappings(tableStructure, fileDescription, baseBulkLoadMapping, currentMapping); let bulkLoadProfileIsModified = false; if (bulkLoadProfileDiffs.length > 0) { bulkLoadProfileIsModified = true; } /******************************************************************************* ** make request to load all saved profiles from backend *******************************************************************************/ async function loadSavedBulkLoadProfiles() { if (!tableMetaData) { return; } const formData = new FormData(); formData.append("tableName", tableMetaData.name); const savedBulkLoadProfiles = await makeSavedBulkLoadProfileRequest("querySavedBulkLoadProfile", formData); const yourSavedBulkLoadProfiles: QRecord[] = []; const bulkLoadProfilesSharedWithYou: QRecord[] = []; for (let i = 0; i < savedBulkLoadProfiles.length; i++) { const record = savedBulkLoadProfiles[i]; if (record.values.get("userId") == currentUserId) { yourSavedBulkLoadProfiles.push(record); } else { bulkLoadProfilesSharedWithYou.push(record); } } setYourSavedBulkLoadProfiles(yourSavedBulkLoadProfiles); setBulkLoadProfilesSharedWithYou(bulkLoadProfilesSharedWithYou); } /******************************************************************************* ** fired when a saved record is clicked from the dropdown *******************************************************************************/ const handleSavedBulkLoadProfileRecordOnClick = async (record: QRecord) => { setSavePopupOpen(false); closeSavedBulkLoadProfilesMenu(); if (bulkLoadProfileOnChangeCallback) { bulkLoadProfileOnChangeCallback(record); } }; /******************************************************************************* ** fired when a save option is selected from the save... button/dropdown combo *******************************************************************************/ const handleDropdownOptionClick = (optionName: string) => { setSaveOptionsOpen(false); setPopupAlertContent(""); closeSavedBulkLoadProfilesMenu(); setSavePopupOpen(true); setIsSaveAsAction(false); setIsRenameAction(false); setIsDeleteAction(false); switch (optionName) { case SAVE_OPTION: if (currentSavedBulkLoadProfileRecord == null) { setSavedBulkLoadProfileNameInputValue(""); } break; case DUPLICATE_OPTION: setSavedBulkLoadProfileNameInputValue(""); setIsSaveAsAction(true); break; case CLEAR_OPTION: setSavePopupOpen(false); if (bulkLoadProfileOnChangeCallback) { bulkLoadProfileOnChangeCallback(null); } break; case RESET_TO_SUGGESTION: setSavePopupOpen(false); if(bulkLoadProfileResetToSuggestedMappingCallback) { bulkLoadProfileResetToSuggestedMappingCallback(); } break; case RENAME_OPTION: if (currentSavedBulkLoadProfileRecord != null) { setSavedBulkLoadProfileNameInputValue(currentSavedBulkLoadProfileRecord.values.get("label")); } setIsRenameAction(true); break; case DELETE_OPTION: setIsDeleteAction(true); break; } }; /******************************************************************************* ** fired when save or delete button saved on confirmation dialogs *******************************************************************************/ async function handleDialogButtonOnClick() { try { setPopupAlertContent(""); setIsSubmitting(true); const formData = new FormData(); if (isDeleteAction) { formData.append("id", currentSavedBulkLoadProfileRecord.values.get("id")); await makeSavedBulkLoadProfileRequest("deleteSavedBulkLoadProfile", formData); setSavePopupOpen(false); setSaveOptionsOpen(false); await (async () => { handleDropdownOptionClick(CLEAR_OPTION); })(); } else { formData.append("tableName", tableMetaData.name); ///////////////////////////////////////////////////////////////////////////////////////// // convert the BulkLoadMapping object to a BulkLoadProfile - the thing that gets saved // ///////////////////////////////////////////////////////////////////////////////////////// const bulkLoadProfile = currentMapping.toProfile(); const mappingJson = JSON.stringify(bulkLoadProfile.profile); formData.append("mappingJson", mappingJson); if (isSaveAsAction || isRenameAction || currentSavedBulkLoadProfileRecord == null) { formData.append("label", savedBulkLoadProfileNameInputValue); if (currentSavedBulkLoadProfileRecord != null && isRenameAction) { formData.append("id", currentSavedBulkLoadProfileRecord.values.get("id")); } } else { formData.append("id", currentSavedBulkLoadProfileRecord.values.get("id")); formData.append("label", currentSavedBulkLoadProfileRecord?.values.get("label")); } const recordList = await makeSavedBulkLoadProfileRequest("storeSavedBulkLoadProfile", formData); await (async () => { if (recordList && recordList.length > 0) { setSavedBulkLoadProfilesHaveLoaded(false); setSavedSuccessMessage("Profile Saved."); setTimeout(() => setSavedSuccessMessage(null), 2500); if (allowSelectingProfile) { loadSavedBulkLoadProfiles(); handleSavedBulkLoadProfileRecordOnClick(recordList[0]); } else { if (bulkLoadProfileOnChangeCallback) { bulkLoadProfileOnChangeCallback(recordList[0]); } } } })(); } setSavePopupOpen(false); setSaveOptionsOpen(false); } catch (e: any) { let message = JSON.stringify(e); if (typeof e == "string") { message = e; } else if (typeof e == "object" && e.message) { message = e.message; } setPopupAlertContent(message); console.log(`Setting error: ${message}`); } finally { setIsSubmitting(false); } } /******************************************************************************* ** stores the current dialog input text to state *******************************************************************************/ const handleSaveDialogInputChange = (event: React.ChangeEvent) => { setSavedBulkLoadProfileNameInputValue(event.target.value); }; /******************************************************************************* ** closes current dialog *******************************************************************************/ const handleSavePopupClose = () => { setSavePopupOpen(false); }; /******************************************************************************* ** make a request to the backend for various savedBulkLoadProfile processes *******************************************************************************/ async function makeSavedBulkLoadProfileRequest(processName: string, formData: FormData): Promise { ///////////////////////// // fetch saved records // ///////////////////////// let savedBulkLoadProfiles = [] as QRecord[]; try { ////////////////////////////////////////////////////////////////// // we don't want this job to go async, so, pass a large timeout // ////////////////////////////////////////////////////////////////// formData.append(QController.STEP_TIMEOUT_MILLIS_PARAM_NAME, 60 * 1000); const processResult = await qController.processInit(processName, formData, qController.defaultMultipartFormDataHeaders()); if (processResult instanceof QJobError) { const jobError = processResult as QJobError; throw (jobError.error); } else { const result = processResult as QJobComplete; if (result.values.savedBulkLoadProfileList) { for (let i = 0; i < result.values.savedBulkLoadProfileList.length; i++) { const qRecord = new QRecord(result.values.savedBulkLoadProfileList[i]); savedBulkLoadProfiles.push(qRecord); } } } } catch (e) { throw (e); } return (savedBulkLoadProfiles); } const hasStorePermission = metaData?.processes.has("storeSavedBulkLoadProfile"); const hasDeletePermission = metaData?.processes.has("deleteSavedBulkLoadProfile"); const hasQueryPermission = metaData?.processes.has("querySavedBulkLoadProfile"); const tooltipMaxWidth = (maxWidth: string) => { return ({ slotProps: { tooltip: { sx: { maxWidth: maxWidth } } } }); }; const menuTooltipAttribs = {...tooltipMaxWidth("250px"), placement: "left", enterDelay: 1000} as TooltipProps; let disabledBecauseNotOwner = false; let notOwnerTooltipText = null; if (currentSavedBulkLoadProfileRecord && currentSavedBulkLoadProfileRecord.values.get("userId") != currentUserId) { disabledBecauseNotOwner = true; notOwnerTooltipText = "You may not save changes to this bulk load profile, because you are not its owner."; } const menuWidth = "300px"; const renderSavedBulkLoadProfilesMenu = tableMetaData && ( { Bulk Load 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.
}
} { !allowSelectingProfile && } { hasStorePermission && Save your current mapping, for quick re-use at a later time.

You will be prompted to enter a name if you choose this option.}> handleDropdownOptionClick(SAVE_OPTION)}> save {currentSavedBulkLoadProfileRecord ? "Save..." : "Save As..."}
} { hasStorePermission && currentSavedBulkLoadProfileRecord != null && handleDropdownOptionClick(RENAME_OPTION)}> edit Rename... } { hasStorePermission && currentSavedBulkLoadProfileRecord != null && handleDropdownOptionClick(DUPLICATE_OPTION)}> content_copy Save As... } { hasDeletePermission && currentSavedBulkLoadProfileRecord != null && handleDropdownOptionClick(DELETE_OPTION)}> delete Delete... } { allowSelectingProfile && handleDropdownOptionClick(CLEAR_OPTION)}> monitor New Bulk Load Profile } { allowSelectingProfile && { } Your Saved Bulk Load Profiles { yourSavedBulkLoadProfiles && yourSavedBulkLoadProfiles.length > 0 ? ( yourSavedBulkLoadProfiles.map((record: QRecord, index: number) => handleSavedBulkLoadProfileRecordOnClick(record)}> {record.values.get("label")} ) ) : ( You do not have any saved bulk load profiles for this table. ) } Bulk Load Profiles Shared with you { bulkLoadProfilesSharedWithYou && bulkLoadProfilesSharedWithYou.length > 0 ? ( bulkLoadProfilesSharedWithYou.map((record: QRecord, index: number) => handleSavedBulkLoadProfileRecordOnClick(record)}> {record.values.get("label")} ) ) : ( You do not have any bulk load profiles shared with you for this table. ) } }
); let buttonText = "Saved Bulk Load Profiles"; let buttonBackground = "none"; let buttonBorder = colors.grayLines.main; let buttonColor = colors.gray.main; if (currentSavedBulkLoadProfileRecord) { if (bulkLoadProfileIsModified) { buttonBackground = accentColorLight; buttonBorder = buttonBackground; buttonColor = accentColor; } else { buttonBackground = accentColor; buttonBorder = buttonBackground; buttonColor = "#FFFFFF"; } } const buttonStyles = { border: `1px solid ${buttonBorder}`, backgroundColor: buttonBackground, color: buttonColor, "&:focus:not(:hover)": { color: buttonColor, backgroundColor: buttonBackground, }, "&:hover": { color: buttonColor, backgroundColor: buttonBackground, } }; /******************************************************************************* ** *******************************************************************************/ function isSaveButtonDisabled(): boolean { if (isSubmitting) { return (true); } const haveInputText = (savedBulkLoadProfileNameInputValue != null && savedBulkLoadProfileNameInputValue.trim() != ""); if (isSaveAsAction || isRenameAction || currentSavedBulkLoadProfileRecord == null) { if (!haveInputText) { return (true); } } return (false); } const linkButtonStyle = { minWidth: "unset", textTransform: "none", fontSize: "0.875rem", fontWeight: "500", padding: "0.5rem" }; return ( hasQueryPermission && tableMetaData ? ( <> {renderSavedBulkLoadProfilesMenu} { savedSuccessMessage && {savedSuccessMessage} } { savedFailedMessage && {savedFailedMessage} } { !currentSavedBulkLoadProfileRecord /*&& bulkLoadProfileIsModified*/ && <> { <> Unsaved Mapping
  • You are not using a saved bulk load profile.
  • { /*bulkLoadProfileDiffs.map((s: string, i: number) =>
  • {s}
  • )*/ }
}>
{/* vertical rule */} {allowSelectingProfile && } } {/* for the no-profile use-case, don't give a reset-link on screens other than the first (file mapping) one - which is tied to the allowSelectingProfile attribute */} {allowSelectingProfile && <> Reset to: } } { currentSavedBulkLoadProfileRecord && bulkLoadProfileIsModified && <> Unsaved Changes
    { bulkLoadProfileDiffs.map((s: string, i: number) =>
  • {s}
  • ) }
{ notOwnerTooltipText && {notOwnerTooltipText} } }> {bulkLoadProfileDiffs.length} Unsaved Change{bulkLoadProfileDiffs.length == 1 ? "" : "s"}
{disabledBecauseNotOwner ? <>   : } {/* vertical rule */} {/* also, don't give a reset-link on screens other than the first (file mapping) one - which is tied to the allowSelectingProfile attribute */} {/* partly because it isn't correctly resetting the values, but also because, it's a litle unclear that what, it would reset changes from other screens too?? */} { allowSelectingProfile && <> } }
{ { //////////////////////////////////////////////////// // make user actually hit delete button // // but for other modes, let Enter submit the form // //////////////////////////////////////////////////// if (e.key == "Enter" && !isDeleteAction) { handleDialogButtonOnClick(); } }} > { currentSavedBulkLoadProfileRecord ? ( isDeleteAction ? ( Delete Bulk Load Profile ) : ( isSaveAsAction ? ( Save Bulk Load Profile As ) : ( isRenameAction ? ( Rename Bulk Load Profile ) : ( Update Existing Bulk Load Profile ) ) ) ) : ( Save New Bulk Load Profile ) } {popupAlertContent ? ( setPopupAlertContent("")}>{popupAlertContent} ) : ("")} { (!currentSavedBulkLoadProfileRecord || isSaveAsAction || isRenameAction) && !isDeleteAction ? ( { isSaveAsAction ? ( Enter a name for this new saved bulk load profile. ) : ( Enter a new name for this saved bulk load profile. ) } { event.target.select(); }} /> ) : ( isDeleteAction ? ( Are you sure you want to delete the bulk load profile {`'${currentSavedBulkLoadProfileRecord?.values.get("label")}'`}? ) : ( Are you sure you want to update the bulk load profile {`'${currentSavedBulkLoadProfileRecord?.values.get("label")}'`}? ) ) } { isDeleteAction ? : } } ) : null ); } export default SavedBulkLoadProfiles;