mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-21 22:58:43 +00:00
Compare commits
5 Commits
feature/18
...
snapshot-f
Author | SHA1 | Date | |
---|---|---|---|
7c7b9a3dbf | |||
1dce760934 | |||
ff4683af1f | |||
ab4be1d5af | |||
0d7e76df6c |
@ -123,7 +123,15 @@ function ChipTextField({...props})
|
||||
setChips(chipData);
|
||||
chipsRef.current = chipData;
|
||||
determineChipColors();
|
||||
}, [JSON.stringify(chipData)]);
|
||||
|
||||
if (chipType !== "pvs")
|
||||
{
|
||||
const currentChipValidity = chips.map((chip, i) =>
|
||||
(chipType !== "number" || !Number.isNaN(Number(chips[i])))
|
||||
);
|
||||
setChipValidity(currentChipValidity);
|
||||
}
|
||||
}, [JSON.stringify(chipData), chips]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
|
@ -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}}}
|
||||
>
|
||||
{
|
||||
<MenuItem sx={{width: menuWidth}} disabled style={{opacity: "initial"}}><b>Bulk Load Profile Actions</b></MenuItem>
|
||||
<MenuItem sx={{width: menuWidth}} disabled style={{opacity: "initial"}}><b>Bulk {bulkAction} Profile Actions</b></MenuItem>
|
||||
}
|
||||
{
|
||||
!allowSelectingProfile &&
|
||||
<MenuItem sx={{width: menuWidth}} disabled style={{opacity: "initial", whiteSpace: "wrap", display: "block"}}>
|
||||
{
|
||||
currentSavedBulkLoadProfileRecord ?
|
||||
<span>You are using the bulk load profile:<br /><b style={{paddingLeft: "1rem"}}>{currentSavedBulkLoadProfileRecord.values.get("label")}</b><br /><br />You can manage this profile on this screen.</span>
|
||||
: <span>You are not using a saved bulk load profile.<br /><br />You can save your profile on this screen.</span>
|
||||
<span>You are using the bulk {bulkAction.toLowerCase()} profile:<br /><b style={{paddingLeft: "1rem"}}>{currentSavedBulkLoadProfileRecord.values.get("label")}</b><br /><br />You can manage this profile on this screen.</span>
|
||||
: <span>You are not using a saved bulk {bulkAction.toLowerCase()} profile.<br /><br />You can save your profile on this screen.</span>
|
||||
}
|
||||
</MenuItem>
|
||||
}
|
||||
@ -456,7 +460,7 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current
|
||||
}
|
||||
{
|
||||
hasStorePermission && currentSavedBulkLoadProfileRecord != null &&
|
||||
<Tooltip {...menuTooltipAttribs} title={notOwnerTooltipText ?? "Change the name for this saved bulk load profile."}>
|
||||
<Tooltip {...menuTooltipAttribs} title={notOwnerTooltipText ?? "Change the name for this saved bulk {bulkAction.toLowerCase()} profile."}>
|
||||
<span>
|
||||
<MenuItem disabled={currentSavedBulkLoadProfileRecord === null || disabledBecauseNotOwner} onClick={() => handleDropdownOptionClick(RENAME_OPTION)}>
|
||||
<ListItemIcon><Icon>edit</Icon></ListItemIcon>
|
||||
@ -467,7 +471,7 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current
|
||||
}
|
||||
{
|
||||
hasStorePermission && currentSavedBulkLoadProfileRecord != null &&
|
||||
<Tooltip {...menuTooltipAttribs} title="Save a new copy this bulk load profile, with a different name, separate from the original.">
|
||||
<Tooltip {...menuTooltipAttribs} title="Save a new copy this bulk {bulkAction.toLowerCase()} profile, with a different name, separate from the original.">
|
||||
<span>
|
||||
<MenuItem disabled={currentSavedBulkLoadProfileRecord === null} onClick={() => handleDropdownOptionClick(DUPLICATE_OPTION)}>
|
||||
<ListItemIcon><Icon>content_copy</Icon></ListItemIcon>
|
||||
@ -478,7 +482,7 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current
|
||||
}
|
||||
{
|
||||
hasDeletePermission && currentSavedBulkLoadProfileRecord != null &&
|
||||
<Tooltip {...menuTooltipAttribs} title={notOwnerTooltipText ?? "Delete this saved bulk load profile."}>
|
||||
<Tooltip {...menuTooltipAttribs} title={notOwnerTooltipText ?? "Delete this saved bulk {bulkAction.toLowerCase()} profile."}>
|
||||
<span>
|
||||
<MenuItem disabled={currentSavedBulkLoadProfileRecord === null || disabledBecauseNotOwner} onClick={() => handleDropdownOptionClick(DELETE_OPTION)}>
|
||||
<ListItemIcon><Icon>delete</Icon></ListItemIcon>
|
||||
@ -489,11 +493,11 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current
|
||||
}
|
||||
{
|
||||
allowSelectingProfile &&
|
||||
<Tooltip {...menuTooltipAttribs} title="Create a new blank bulk load profile for this table, removing all mappings.">
|
||||
<Tooltip {...menuTooltipAttribs} title="Create a new blank bulk {bulkAction.toLowerCase()} profile for this table, removing all mappings.">
|
||||
<span>
|
||||
<MenuItem onClick={() => handleDropdownOptionClick(CLEAR_OPTION)}>
|
||||
<ListItemIcon><Icon>monitor</Icon></ListItemIcon>
|
||||
New Bulk Load Profile
|
||||
New Bulk {bulkAction} Profile
|
||||
</MenuItem>
|
||||
</span>
|
||||
</Tooltip>
|
||||
@ -504,7 +508,7 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current
|
||||
{
|
||||
<Divider />
|
||||
}
|
||||
<MenuItem disabled style={{"opacity": "initial"}}><b>Your Saved Bulk Load Profiles</b></MenuItem>
|
||||
<MenuItem disabled style={{"opacity": "initial"}}><b>Your Saved Bulk {bulkAction} Profiles</b></MenuItem>
|
||||
{
|
||||
yourSavedBulkLoadProfiles && yourSavedBulkLoadProfiles.length > 0 ? (
|
||||
yourSavedBulkLoadProfiles.map((record: QRecord, index: number) =>
|
||||
@ -514,11 +518,11 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current
|
||||
)
|
||||
) : (
|
||||
<MenuItem disabled sx={{opacity: "1 !important"}}>
|
||||
<i>You do not have any saved bulk load profiles for this table.</i>
|
||||
<i>You do not have any saved bulk {bulkAction.toLowerCase()} profiles for this table.</i>
|
||||
</MenuItem>
|
||||
)
|
||||
}
|
||||
<MenuItem disabled style={{"opacity": "initial"}}><b>Bulk Load Profiles Shared with you</b></MenuItem>
|
||||
<MenuItem disabled style={{"opacity": "initial"}}><b>Bulk {bulkAction} Profiles Shared with you</b></MenuItem>
|
||||
{
|
||||
bulkLoadProfilesSharedWithYou && bulkLoadProfilesSharedWithYou.length > 0 ? (
|
||||
bulkLoadProfilesSharedWithYou.map((record: QRecord, index: number) =>
|
||||
@ -528,7 +532,7 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current
|
||||
)
|
||||
) : (
|
||||
<MenuItem disabled sx={{opacity: "1 !important"}}>
|
||||
<i>You do not have any bulk load profiles shared with you for this table.</i>
|
||||
<i>You do not have any bulk {bulkAction.toLowerCase()} profiles shared with you for this table.</i>
|
||||
</MenuItem>
|
||||
)
|
||||
}
|
||||
@ -537,7 +541,7 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current
|
||||
</Menu>
|
||||
);
|
||||
|
||||
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
|
||||
<Tooltip {...tooltipMaxWidth("24rem")} sx={{cursor: "pointer"}} title={<>
|
||||
<b>Unsaved Mapping</b>
|
||||
<ul style={{padding: "0.5rem 1rem"}}>
|
||||
<li>You are not using a saved bulk load profile.</li>
|
||||
<li>You are not using a saved bulk {bulkAction.toLowerCase()} profile.</li>
|
||||
{
|
||||
/*bulkLoadProfileDiffs.map((s: string, i: number) => <li key={i}>{s}</li>)*/
|
||||
}
|
||||
</ul>
|
||||
</>}>
|
||||
<Button disableRipple={true} sx={linkButtonStyle} onClick={() => handleDropdownOptionClick(SAVE_OPTION)}>Save Bulk Load Profile As…</Button>
|
||||
<Button disableRipple={true} sx={linkButtonStyle} onClick={() => handleDropdownOptionClick(SAVE_OPTION)}>Save Bulk {bulkAction} Profile As…</Button>
|
||||
</Tooltip>
|
||||
|
||||
{/* vertical rule */}
|
||||
@ -716,20 +720,20 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current
|
||||
{
|
||||
currentSavedBulkLoadProfileRecord ? (
|
||||
isDeleteAction ? (
|
||||
<DialogTitle id="alert-dialog-title">Delete Bulk Load Profile</DialogTitle>
|
||||
<DialogTitle id="alert-dialog-title">Delete Bulk {bulkAction} Profile</DialogTitle>
|
||||
) : (
|
||||
isSaveAsAction ? (
|
||||
<DialogTitle id="alert-dialog-title">Save Bulk Load Profile As</DialogTitle>
|
||||
<DialogTitle id="alert-dialog-title">Save Bulk {bulkAction} Profile As</DialogTitle>
|
||||
) : (
|
||||
isRenameAction ? (
|
||||
<DialogTitle id="alert-dialog-title">Rename Bulk Load Profile</DialogTitle>
|
||||
<DialogTitle id="alert-dialog-title">Rename Bulk {bulkAction} Profile</DialogTitle>
|
||||
) : (
|
||||
<DialogTitle id="alert-dialog-title">Update Existing Bulk Load Profile</DialogTitle>
|
||||
<DialogTitle id="alert-dialog-title">Update Existing Bulk {bulkAction} Profile</DialogTitle>
|
||||
)
|
||||
)
|
||||
)
|
||||
) : (
|
||||
<DialogTitle id="alert-dialog-title">Save New Bulk Load Profile</DialogTitle>
|
||||
<DialogTitle id="alert-dialog-title">Save New Bulk {bulkAction} Profile</DialogTitle>
|
||||
)
|
||||
}
|
||||
<DialogContent sx={{width: "500px"}}>
|
||||
@ -743,15 +747,15 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current
|
||||
<Box>
|
||||
{
|
||||
isSaveAsAction ? (
|
||||
<Box mb={3}>Enter a name for this new saved bulk load profile.</Box>
|
||||
<Box mb={3}>Enter a name for this new saved bulk {bulkAction.toLowerCase()} profile.</Box>
|
||||
) : (
|
||||
<Box mb={3}>Enter a new name for this saved bulk load profile.</Box>
|
||||
<Box mb={3}>Enter a new name for this saved bulk {bulkAction.toLowerCase()} profile.</Box>
|
||||
)
|
||||
}
|
||||
<TextField
|
||||
autoFocus
|
||||
name="custom-delimiter-value"
|
||||
placeholder="Bulk Load Profile Name"
|
||||
placeholder={`Bulk ${bulkAction} Profile Name`}
|
||||
inputProps={{width: "100%", maxLength: 100}}
|
||||
value={savedBulkLoadProfileNameInputValue}
|
||||
sx={{width: "100%"}}
|
||||
@ -764,9 +768,9 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current
|
||||
</Box>
|
||||
) : (
|
||||
isDeleteAction ? (
|
||||
<Box>Are you sure you want to delete the bulk load profile {`'${currentSavedBulkLoadProfileRecord?.values.get("label")}'`}?</Box>
|
||||
<Box>Are you sure you want to delete the bulk {bulkAction.toLowerCase()} profile {`'${currentSavedBulkLoadProfileRecord?.values.get("label")}'`}?</Box>
|
||||
) : (
|
||||
<Box>Are you sure you want to update the bulk load profile {`'${currentSavedBulkLoadProfileRecord?.values.get("label")}'`}?</Box>
|
||||
<Box>Are you sure you want to update the bulk {bulkAction.toLowerCase()} profile {`'${currentSavedBulkLoadProfileRecord?.values.get("label")}'`}?</Box>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -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
|
||||
<Box ml="1rem">
|
||||
{
|
||||
valueType == "column" && <>
|
||||
<Box>
|
||||
<Box display="flex" alignItems="center" sx={{height: "45px"}}>
|
||||
<FormControlLabel value="mapValues" control={<Checkbox size="small" defaultChecked={bulkLoadField.doValueMapping} onChange={(event, checked) => mapValuesChanged(checked)} />} label={"Map values"} sx={{minWidth: "140px", whiteSpace: "nowrap"}} />
|
||||
{
|
||||
isBulkEdit && !isRequired && <FormControlLabel value="clearIfEmpty" control={<Checkbox size="small" defaultChecked={bulkLoadField.clearIfEmpty} onChange={(event, checked) => clearIfEmptyChanged(checked)} />} label={"Clear if empty"} sx={{minWidth: "140px", whiteSpace: "nowrap"}} />
|
||||
}
|
||||
</Box>
|
||||
<Box fontSize={mainFontSize} mt="0.5rem">
|
||||
Preview Values: <span style={{color: "gray"}}>{(fileDescription.getPreviewValues(selectedColumn?.value) ?? [""]).join(", ")}</span>
|
||||
|
@ -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 (
|
||||
<>
|
||||
<h5>Required Fields</h5>
|
||||
{isBulkEdit ? <h5>Key Fields</h5> : <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>
|
||||
(
|
||||
isBulkEdit ?
|
||||
<i style={{fontSize: "0.875rem"}}>Select table key fields to continue.</i>
|
||||
:
|
||||
<i style={{fontSize: "0.875rem"}}>There are no required fields in this table.</i>
|
||||
)
|
||||
}
|
||||
{bulkLoadMapping.requiredFields.map((bulkLoadField) => (
|
||||
<BulkLoadFileMappingField
|
||||
@ -267,12 +273,13 @@ export default function BulkLoadFileMappingFields({bulkLoadMapping, fileDescript
|
||||
bulkLoadField={bulkLoadField}
|
||||
isRequired={true}
|
||||
forceParentUpdate={forceParentUpdate}
|
||||
isBulkEdit={isBulkEdit}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
<Box mt="1rem">
|
||||
<h5>Additional Fields</h5>
|
||||
{isBulkEdit ? <h5>Fields To Update</h5> : <h5>Additional Fields</h5>}
|
||||
<Box pl={"1rem"}>
|
||||
{bulkLoadMapping.additionalFields.map((bulkLoadField) => (
|
||||
<BulkLoadFileMappingField
|
||||
@ -282,6 +289,7 @@ export default function BulkLoadFileMappingFields({bulkLoadMapping, fileDescript
|
||||
isRequired={false}
|
||||
removeFieldCallback={() => removeField(bulkLoadField)}
|
||||
forceParentUpdate={forceParentUpdate}
|
||||
isBulkEdit={isBulkEdit}
|
||||
/>
|
||||
))}
|
||||
|
||||
|
@ -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>(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,7 +152,7 @@ const BulkLoadFileMappingForm = forwardRef(({processValues, tableMetaData, metaD
|
||||
setNoMappedFieldsError(null);
|
||||
}
|
||||
|
||||
if(haveProfileErrors)
|
||||
if (haveProfileErrors)
|
||||
{
|
||||
setTimeout(() =>
|
||||
{
|
||||
@ -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}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<BulkLoadMappingHeader
|
||||
tableMetaData={tableMetaData}
|
||||
isBulkEdit={processValues.isBulkEdit}
|
||||
key={rerenderHeader}
|
||||
bulkLoadMapping={bulkLoadMapping}
|
||||
fileDescription={fileDescription}
|
||||
@ -245,6 +261,7 @@ const BulkLoadFileMappingForm = forwardRef(({processValues, tableMetaData, metaD
|
||||
|
||||
<Box mt="2rem">
|
||||
<BulkLoadFileMappingFields
|
||||
isBulkEdit={processValues.isBulkEdit}
|
||||
bulkLoadMapping={bulkLoadMapping}
|
||||
fileDescription={fileDescription}
|
||||
forceParentUpdate={() =>
|
||||
@ -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<string, string>;
|
||||
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")}
|
||||
</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>)}
|
||||
disableClearable
|
||||
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>
|
||||
!isBulkEdit ? (
|
||||
<>
|
||||
<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>)}
|
||||
disableClearable
|
||||
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>
|
||||
}
|
||||
{getFormattedHelpContent("layout")}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{
|
||||
dynamicField &&
|
||||
<>
|
||||
<DynamicFormFieldLabel name={dynamicField.name} label={`${dynamicField.label} *`} />
|
||||
<QDynamicFormField name={dynamicField.name} displayFormat={""} label={""} formFieldObject={dynamicField} type={"pvs"} value={bulkLoadMapping.keyFields} onChangeCallback={keyFieldsChanged} />
|
||||
{
|
||||
fieldErrors.keyFields &&
|
||||
<MDTypography component="div" variant="caption" color="error" fontWeight="regular" mt="0.25rem">
|
||||
{<div className="fieldErrorMessage">{fieldErrors.keyFields}</div>}
|
||||
</MDTypography>
|
||||
}
|
||||
{getFormattedHelpContent("tableKeyFields")}
|
||||
</>
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
{getFormattedHelpContent("layout")}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
@ -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 = <Tooltip title="This column header is a duplicate. Only the first occurrance of it will be used." placement="top" enterDelay={500}>
|
||||
<Icon color="warning" sx={{p: "0.125rem", mr: "0.25rem"}}>warning</Icon>
|
||||
</Tooltip>
|
||||
</Tooltip>;
|
||||
}
|
||||
|
||||
return (<td key={letter} style={{textAlign: "center", color: getHeaderColor(count), cursor: getCursor(count)}}>
|
||||
<>
|
||||
{
|
||||
count > 0 &&
|
||||
<Tooltip title={getColumnTooltip(fields)} placement="top" enterDelay={500}>
|
||||
<Box>
|
||||
{dupeWarning}
|
||||
{letter}
|
||||
<Badge badgeContent={count} variant={"standard"} color="secondary" sx={{marginTop: ".75rem"}}><Icon></Icon></Badge>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
<Tooltip title={getColumnTooltip(fields)} placement="top" enterDelay={500}>
|
||||
<Box>
|
||||
{dupeWarning}
|
||||
{letter}
|
||||
<Badge badgeContent={count} variant={"standard"} color="secondary" sx={{marginTop: ".75rem"}}><Icon></Icon></Badge>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
}
|
||||
{
|
||||
count == 0 && <Box>{dupeWarning}{letter}</Box>
|
||||
@ -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 <td key={value} style={tdStyle}>
|
||||
<Tooltip title={getColumnTooltip(fields)} placement="top" enterDelay={500}><Box>{value}</Box></Tooltip>
|
||||
</td>
|
||||
</td>;
|
||||
}
|
||||
else
|
||||
{
|
||||
return <td key={value} style={tdStyle}>{value}</td>
|
||||
return <td key={value} style={tdStyle}>{value}</td>;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return <td key={value} style={tdStyle}>{value}</td>
|
||||
return <td key={value} style={tdStyle}>{value}</td>;
|
||||
}
|
||||
}
|
||||
)}
|
||||
|
@ -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<QRecord>(savedBulkLoadProfileRecord));
|
||||
|
||||
const [fileDescription] = useState(new FileDescription(processValues.headerValues, processValues.headerLetters, processValues.bodyValuesPreview));
|
||||
@ -93,6 +93,7 @@ const BulkLoadProfileForm = forwardRef(({processValues, tableMetaData, metaData}
|
||||
allowSelectingProfile={false}
|
||||
fileDescription={fileDescription}
|
||||
bulkLoadProfileOnChangeCallback={bulkLoadProfileOnChangeCallback}
|
||||
isBulkEdit={processValues.isBulkEdit}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
|
@ -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}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
|
@ -342,7 +342,7 @@ function FilterCriteriaPaster({table, field, type, onSave}: Props): JSX.Element
|
||||
if (type === "number")
|
||||
{
|
||||
let suffix = invalidCount === 1 ? " value is not a number" : " values are not numbers";
|
||||
setErrorText(invalidCount + suffix + "numbers and will not be added to the filter");
|
||||
setErrorText(invalidCount + suffix + " and will not be added to the filter");
|
||||
}
|
||||
else if (type === "pvs")
|
||||
{
|
||||
@ -363,7 +363,7 @@ function FilterCriteriaPaster({table, field, type, onSave}: Props): JSX.Element
|
||||
return (
|
||||
<Box>
|
||||
<Tooltip title="Quickly add many values to your filter by pasting them from a spreadsheet or any other data source.">
|
||||
<Icon onClick={handlePasteClick} fontSize="small" color="info" sx={{mx: 0.25, cursor: "pointer"}}>paste_content</Icon>
|
||||
<Icon className="criteriaPasterButton" onClick={handlePasteClick} fontSize="small" color="info" sx={{mx: 0.25, cursor: "pointer"}}>paste_content</Icon>
|
||||
</Tooltip>
|
||||
{
|
||||
pasteModalIsOpen &&
|
||||
@ -391,6 +391,7 @@ function FilterCriteriaPaster({table, field, type, onSave}: Props): JSX.Element
|
||||
<Grid item pr={3} xs={6} lg={6} sx={{width: "100%", display: "flex", flexDirection: "column", flexGrow: 1}}>
|
||||
<FormControl sx={{m: 1, width: "100%"}}>
|
||||
<TextField
|
||||
className="criteriaPasterTextArea"
|
||||
id="outlined-multiline-static"
|
||||
label="PASTE TEXT"
|
||||
multiline
|
||||
|
@ -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(<MenuItem key="bulkEdit" onClick={() => runSomething(bulkEditClicked)}><ListItemIcon><Icon>edit</Icon></ListItemIcon>Bulk Edit</MenuItem>);
|
||||
menuItems.push(<MenuItem key="bulkEditWithFile" onClick={() => runSomething(bulkEditWithFileClicked)}><ListItemIcon><Icon>edit_note</Icon></ListItemIcon>Bulk Edit With File</MenuItem>);
|
||||
}
|
||||
if (tableMetaData.capabilities.has(Capability.TABLE_DELETE) && tableMetaData.deletePermission)
|
||||
{
|
||||
@ -130,5 +131,5 @@ export default function QueryScreenActionMenu({metaData, tableMetaData, tablePro
|
||||
{menuItems}
|
||||
</Menu>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -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 }
|
||||
};
|
||||
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -103,6 +103,30 @@ public class QueryScreenLib
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void openCriteriaPasterAndPasteValues(String fieldName, List<String> values)
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// open the is any of criteria for given field and click the paster button //
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
qSeleniumLib.waitForSelectorContaining("BUTTON", fieldName).click();
|
||||
qSeleniumLib.waitForSelector("#criteriaOperator").click();
|
||||
qSeleniumLib.waitForSelectorContaining("LI", "is any of").click();
|
||||
qSeleniumLib.waitForMillis(250);
|
||||
qSeleniumLib.waitForSelector(".criteriaPasterButton").click();
|
||||
|
||||
////////////////////////////////////////
|
||||
// paste the values into the textarea //
|
||||
////////////////////////////////////////
|
||||
qSeleniumLib
|
||||
.waitForSelector(".criteriaPasterTextArea textarea#outlined-multiline-static")
|
||||
.sendKeys(String.join("\n", values));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -22,13 +22,16 @@
|
||||
package com.kingsrook.qqq.frontend.materialdashboard.selenium.tests.query;
|
||||
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import com.kingsrook.qqq.frontend.materialdashboard.selenium.lib.QBaseSeleniumTest;
|
||||
import com.kingsrook.qqq.frontend.materialdashboard.selenium.lib.QQQMaterialDashboardSelectors;
|
||||
import com.kingsrook.qqq.frontend.materialdashboard.selenium.lib.QueryScreenLib;
|
||||
import com.kingsrook.qqq.frontend.materialdashboard.selenium.lib.javalin.CapturedContext;
|
||||
import com.kingsrook.qqq.frontend.materialdashboard.selenium.lib.javalin.QSeleniumJavalin;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.openqa.selenium.WebElement;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
|
||||
@ -200,6 +203,204 @@ public class QueryScreenTest extends QBaseSeleniumTest
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testCriteriaPasterHappyPath()
|
||||
{
|
||||
QueryScreenLib queryScreenLib = new QueryScreenLib(qSeleniumLib);
|
||||
|
||||
////////////////////////////
|
||||
// go to the person page //
|
||||
////////////////////////////
|
||||
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/peopleApp/greetingsApp/person", "Person");
|
||||
queryScreenLib.waitForQueryToHaveRan();
|
||||
|
||||
//////////////////////////////////////
|
||||
// open the paste values dialog UI //
|
||||
//////////////////////////////////////
|
||||
queryScreenLib.openCriteriaPasterAndPasteValues("id", List.of("1", "2", "3"));
|
||||
|
||||
///////////////////////////////////////////////////////////////
|
||||
// wait for chips to appear in the filter values review box //
|
||||
///////////////////////////////////////////////////////////////
|
||||
assertFilterPasterChipCounts(3, 0);
|
||||
|
||||
///////////////////////////////////////////////
|
||||
// confirm each chip has the blue color class //
|
||||
///////////////////////////////////////////////
|
||||
qSeleniumLib.waitForSelectorAll(".MuiChip-root", 3).forEach(chip ->
|
||||
{
|
||||
String classAttr = chip.getAttribute("class");
|
||||
assertThat(classAttr).contains("MuiChip-colorInfo");
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testCriteriaPasterInvalidValueValidation()
|
||||
{
|
||||
QueryScreenLib queryScreenLib = new QueryScreenLib(qSeleniumLib);
|
||||
|
||||
////////////////////////////
|
||||
// go to the person page //
|
||||
////////////////////////////
|
||||
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/peopleApp/greetingsApp/person", "Person");
|
||||
queryScreenLib.waitForQueryToHaveRan();
|
||||
|
||||
//////////////////////////////////////
|
||||
// open the paste values dialog UI //
|
||||
//////////////////////////////////////
|
||||
queryScreenLib.openCriteriaPasterAndPasteValues("id", List.of("1", "a", "3"));
|
||||
|
||||
//////////////////////////////////////////////////////
|
||||
// check that chips match values and are classified //
|
||||
//////////////////////////////////////////////////////
|
||||
assertFilterPasterChipCounts(2, 1);
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// confirm that an appropriate validation error message is shown //
|
||||
////////////////////////////////////////////////////////////////////
|
||||
WebElement errorMessage = qSeleniumLib.waitForSelectorContaining("span", "value is not a number");
|
||||
assertThat(errorMessage.getText()).contains("value is not a number");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testCriteriaPasterDuplicateValueValidation()
|
||||
{
|
||||
QueryScreenLib queryScreenLib = new QueryScreenLib(qSeleniumLib);
|
||||
|
||||
////////////////////////////
|
||||
// go to the person page //
|
||||
////////////////////////////
|
||||
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/peopleApp/greetingsApp/person", "Person");
|
||||
queryScreenLib.waitForQueryToHaveRan();
|
||||
|
||||
//////////////////////////////////////
|
||||
// open the paste values dialog UI //
|
||||
//////////////////////////////////////
|
||||
List<String> pastedValues = List.of("1", "1", "1", "2", "2");
|
||||
queryScreenLib.openCriteriaPasterAndPasteValues("id", pastedValues);
|
||||
|
||||
///////////////////////////////////////////////
|
||||
// expected chip & uniqueness calculations //
|
||||
///////////////////////////////////////////////
|
||||
int totalCount = pastedValues.size(); // 5
|
||||
int uniqueCount = new HashSet<>(pastedValues).size(); // 2
|
||||
|
||||
/////////////////////////////
|
||||
// chips should show dupes //
|
||||
/////////////////////////////
|
||||
assertFilterPasterChipCounts(pastedValues.size(), 0);
|
||||
|
||||
////////////////////////////////////////////////////////////////
|
||||
// counter text should match “5 values (2 unique)” (or alike) //
|
||||
////////////////////////////////////////////////////////////////
|
||||
String expectedCounter = totalCount + " values (" + uniqueCount + " unique)";
|
||||
WebElement counterLabel = qSeleniumLib.waitForSelectorContaining("span", "unique");
|
||||
assertThat(counterLabel.getText()).contains(expectedCounter);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testCriteriaPasterWithPVSHappyPath()
|
||||
{
|
||||
QueryScreenLib queryScreenLib = new QueryScreenLib(qSeleniumLib);
|
||||
|
||||
////////////////////////////
|
||||
// go to the person page //
|
||||
////////////////////////////
|
||||
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/peopleApp/greetingsApp/person", "Person");
|
||||
queryScreenLib.waitForQueryToHaveRan();
|
||||
|
||||
//////////////////////////////////////
|
||||
// open the paste values dialog UI //
|
||||
//////////////////////////////////////
|
||||
queryScreenLib.addBasicFilter("home city");
|
||||
queryScreenLib.openCriteriaPasterAndPasteValues("home city", List.of("St. Louis", "chesterfield"));
|
||||
qSeleniumLib.waitForSeconds(1);
|
||||
|
||||
///////////////////////////////////////////////////////////////
|
||||
// wait for chips to appear in the filter values review box //
|
||||
///////////////////////////////////////////////////////////////
|
||||
assertFilterPasterChipCounts(2, 0);
|
||||
|
||||
///////////////////////////////////////////////
|
||||
// confirm each chip has the blue color class //
|
||||
///////////////////////////////////////////////
|
||||
qSeleniumLib.waitForSelectorAll(".MuiChip-root", 2).forEach(chip ->
|
||||
{
|
||||
String classAttr = chip.getAttribute("class");
|
||||
assertThat(classAttr).contains("MuiChip-colorInfo");
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testCriteriaPasterWithPVSTwoGoodOneBadAndDupes()
|
||||
{
|
||||
QueryScreenLib queryScreenLib = new QueryScreenLib(qSeleniumLib);
|
||||
|
||||
////////////////////////////
|
||||
// go to the person page //
|
||||
////////////////////////////
|
||||
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/peopleApp/greetingsApp/person", "Person");
|
||||
queryScreenLib.waitForQueryToHaveRan();
|
||||
|
||||
//////////////////////////////////////
|
||||
// open the paste values dialog UI //
|
||||
//////////////////////////////////////
|
||||
List<String> cities = List.of("St. Louis", "chesterfield", "Maryville", "st. louis", "st. louis", "chesterfield");
|
||||
queryScreenLib.addBasicFilter("home city");
|
||||
queryScreenLib.openCriteriaPasterAndPasteValues("home city", cities);
|
||||
qSeleniumLib.waitForSeconds(1);
|
||||
|
||||
///////////////////////////////////////////////
|
||||
// expected chip & uniqueness calculations //
|
||||
///////////////////////////////////////////////
|
||||
int totalCount = cities.size();
|
||||
int uniqueCount = cities.stream().map(String::toLowerCase).collect(Collectors.toSet()).size();
|
||||
|
||||
///////////////////////////////////////////
|
||||
// chips should show dupes and bad chips //
|
||||
///////////////////////////////////////////
|
||||
assertFilterPasterChipCounts(5, 1);
|
||||
|
||||
////////////////////////////////////////////////////////////////
|
||||
// counter text should match “5 values (2 unique)” (or alike) //
|
||||
////////////////////////////////////////////////////////////////
|
||||
String expectedCounter = totalCount + " values (" + uniqueCount + " unique)";
|
||||
WebElement counterLabel = qSeleniumLib.waitForSelectorContaining("span", "unique");
|
||||
assertThat(counterLabel.getText()).contains(expectedCounter);
|
||||
|
||||
//////////////////////////////////////////
|
||||
// assert the "value not found" warning //
|
||||
//////////////////////////////////////////
|
||||
WebElement warning = qSeleniumLib.waitForSelectorContaining("span", "was not found");
|
||||
assertThat(warning.getText()).contains("1 value was not found and will not be added to the filter");
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -273,4 +474,18 @@ public class QueryScreenTest extends QBaseSeleniumTest
|
||||
queryScreenLib.clickAdvancedFilterClearIcon();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void assertFilterPasterChipCounts(int expectedValid, int expectedInvalid)
|
||||
{
|
||||
List<WebElement> chips = qSeleniumLib.waitForSelectorAll(".MuiChip-root", expectedValid + expectedInvalid);
|
||||
long validCount = chips.stream().filter(c -> c.getAttribute("class").contains("MuiChip-colorInfo")).count();
|
||||
long errorCount = chips.stream().filter(c -> c.getAttribute("class").contains("MuiChip-colorError")).count();
|
||||
|
||||
assertThat(validCount).isEqualTo(expectedValid);
|
||||
assertThat(errorCount).isEqualTo(expectedInvalid);
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user