Compare commits

..

4 Commits

19 changed files with 640 additions and 1097 deletions

1
.gitignore vendored
View File

@ -31,3 +31,4 @@ yalc.lock
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
/src/main/resources/material-dashboard/

View File

@ -154,7 +154,7 @@ material-dashboard-2-pro-react-ts
│ │   ├── Cards │ │   ├── Cards
│ │   ├── Charts │ │   ├── Charts
│ │   ├── Configurator │ │   ├── Configurator
│ │   ├── Footer │ │   ├── FooterCard
│ │   ├── Items │ │   ├── Items
│ │   ├── LayoutContainers │ │   ├── LayoutContainers
│ │   ├── Lists │ │   ├── Lists

1002
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -23,6 +23,8 @@
"@types/react-dom": "18.0.0", "@types/react-dom": "18.0.0",
"@types/react-router-hash-link": "2.4.5", "@types/react-router-hash-link": "2.4.5",
"ace-builds": "1.12.3", "ace-builds": "1.12.3",
"ajv": "^8.11.0",
"ajv-keywords": "^5.1.0",
"chart.js": "3.4.1", "chart.js": "3.4.1",
"chroma-js": "2.4.2", "chroma-js": "2.4.2",
"cmdk": "0.2.0", "cmdk": "0.2.0",
@ -35,8 +37,8 @@
"html-react-parser": "1.4.8", "html-react-parser": "1.4.8",
"html-to-text": "^9.0.5", "html-to-text": "^9.0.5",
"http-proxy-middleware": "2.0.6", "http-proxy-middleware": "2.0.6",
"lodash": "4.17.21",
"jwt-decode": "3.1.2", "jwt-decode": "3.1.2",
"lodash": "4.17.21",
"oidc-client-ts": "2.4.1", "oidc-client-ts": "2.4.1",
"rapidoc": "9.3.4", "rapidoc": "9.3.4",
"react": "18.0.0", "react": "18.0.0",
@ -60,14 +62,15 @@
"yup": "0.32.11" "yup": "0.32.11"
}, },
"scripts": { "scripts": {
"build": "react-scripts build", "build": "PUBLIC_URL=. react-scripts build",
"clean": "rm -rf node_modules package-lock.json lib", "clean": "rm -rf node_modules package-lock.json lib",
"eject": "react-scripts eject", "eject": "react-scripts eject",
"clean-and-install": "rm -rf node_modules/ && rm -rf package-lock.json && rm -rf lib/ && npm install --legacy-peer-deps && npm dedupe --force", "clean-and-install": "rm -rf node_modules/ && rm -rf package-lock.json && rm -rf lib/ && npm install --legacy-peer-deps && npm dedupe --force",
"npm-install": "npm install --legacy-peer-deps", "npm-install": "npm install --legacy-peer-deps",
"prepublishOnly": "tsc -p ./ --outDir lib/", "prepublishOnly": "tsc -p ./ --outDir lib/",
"start": "BROWSER=none react-scripts --max-http-header-size=65535 start", "start": "PUBLIC_URL=. BROWSER=none react-scripts --max-http-header-size=65535 start",
"test": "react-scripts test" "test": "react-scripts test",
"export": "rm -rf dist && PUBLIC_URL=. react-scripts build && rm -rf src/main/resources/material-dashboard && mkdir -p src/main/resources/material-dashboard && cp -r build/* src/main/resources/material-dashboard"
}, },
"eslintConfig": { "eslintConfig": {
"extends": [ "extends": [

View File

@ -66,7 +66,7 @@
<dependency> <dependency>
<groupId>com.kingsrook.qqq</groupId> <groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-backend-core</artifactId> <artifactId>qqq-backend-core</artifactId>
<version>0.25.0-integration-sprint-62-20250307-205536</version> <version>0.26.0-SNAPSHOT</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.slf4j</groupId> <groupId>org.slf4j</groupId>

View File

@ -36,6 +36,20 @@ import {BrowserRouter} from "react-router-dom";
const qController = Client.getInstance(); const qController = Client.getInstance();
function getBasePath(): string
{
// You can change this logic depending on how you detect your mount point
const path = window.location.pathname;
console.warn("Using hacked base path for QQQ application, please update this code to be better : path ["+ path +"].");
// Example: If app is deployed at /admin or /portal
if (path.startsWith("/admin")) return "/admin";
if (path.startsWith("/portal")) return "/portal"; // TODO: This is all temporary, we need to fix this properly
return "/";
}
if (document.location.search && document.location.search.indexOf("clearAuthenticationMetaDataLocalStorage") > -1) if (document.location.search && document.location.search.indexOf("clearAuthenticationMetaDataLocalStorage") > -1)
{ {
qController.clearAuthenticationMetaDataLocalStorage(); qController.clearAuthenticationMetaDataLocalStorage();
@ -89,19 +103,19 @@ authenticationMetaDataPromise.then((authenticationMetaData) =>
if (authenticationMetaData.type === "AUTH_0") if (authenticationMetaData.type === "AUTH_0")
{ {
root.render(<BrowserRouter> root.render(<BrowserRouter basename={getBasePath()}>
<Auth0RouterBody /> <Auth0RouterBody />
</BrowserRouter>); </BrowserRouter>);
} }
else if (authenticationMetaData.type === "OAUTH2") else if (authenticationMetaData.type === "OAUTH2")
{ {
root.render(<BrowserRouter> root.render(<BrowserRouter basename={getBasePath()}>
<OAuth2RouterBody /> <OAuth2RouterBody />
</BrowserRouter>); </BrowserRouter>);
} }
else if (authenticationMetaData.type === "FULLY_ANONYMOUS" || authenticationMetaData.type === "MOCK") else if (authenticationMetaData.type === "FULLY_ANONYMOUS" || authenticationMetaData.type === "MOCK")
{ {
root.render(<BrowserRouter> root.render(<BrowserRouter basename={getBasePath()}>
<AnonymousRouterBody /> <AnonymousRouterBody />
</BrowserRouter>); </BrowserRouter>);
} }

View File

@ -123,15 +123,7 @@ function ChipTextField({...props})
setChips(chipData); setChips(chipData);
chipsRef.current = chipData; chipsRef.current = chipData;
determineChipColors(); 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(() => useEffect(() =>
{ {

View File

@ -59,8 +59,7 @@ interface Props
bulkLoadProfileOnChangeCallback?: (record: QRecord | null) => void, bulkLoadProfileOnChangeCallback?: (record: QRecord | null) => void,
allowSelectingProfile?: boolean, allowSelectingProfile?: boolean,
fileDescription?: FileDescription, fileDescription?: FileDescription,
bulkLoadProfileResetToSuggestedMappingCallback?: () => void, bulkLoadProfileResetToSuggestedMappingCallback?: () => void
isBulkEdit?: boolean;
} }
SavedBulkLoadProfiles.defaultProps = { SavedBulkLoadProfiles.defaultProps = {
@ -73,7 +72,7 @@ const qController = Client.getInstance();
** menu-button, text elements, and modal(s) that let you work with saved ** menu-button, text elements, and modal(s) that let you work with saved
** bulk-load profiles. ** bulk-load profiles.
***************************************************************************/ ***************************************************************************/
function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, currentSavedBulkLoadProfileRecord, bulkLoadProfileOnChangeCallback, currentMapping, allowSelectingProfile, fileDescription, bulkLoadProfileResetToSuggestedMappingCallback, isBulkEdit}: Props): JSX.Element function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, currentSavedBulkLoadProfileRecord, bulkLoadProfileOnChangeCallback, currentMapping, allowSelectingProfile, fileDescription, bulkLoadProfileResetToSuggestedMappingCallback}: Props): JSX.Element
{ {
const [yourSavedBulkLoadProfiles, setYourSavedBulkLoadProfiles] = useState([] as QRecord[]); const [yourSavedBulkLoadProfiles, setYourSavedBulkLoadProfiles] = useState([] as QRecord[]);
const [bulkLoadProfilesSharedWithYou, setBulkLoadProfilesSharedWithYou] = useState([] as QRecord[]); const [bulkLoadProfilesSharedWithYou, setBulkLoadProfilesSharedWithYou] = useState([] as QRecord[]);
@ -143,7 +142,6 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current
const formData = new FormData(); const formData = new FormData();
formData.append("tableName", tableMetaData.name); formData.append("tableName", tableMetaData.name);
formData.append("isBulkEdit", isBulkEdit.toString());
const savedBulkLoadProfiles = await makeSavedBulkLoadProfileRequest("querySavedBulkLoadProfile", formData); const savedBulkLoadProfiles = await makeSavedBulkLoadProfileRequest("querySavedBulkLoadProfile", formData);
const yourSavedBulkLoadProfiles: QRecord[] = []; const yourSavedBulkLoadProfiles: QRecord[] = [];
@ -214,7 +212,7 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current
break; break;
case RESET_TO_SUGGESTION: case RESET_TO_SUGGESTION:
setSavePopupOpen(false); setSavePopupOpen(false);
if (bulkLoadProfileResetToSuggestedMappingCallback) if(bulkLoadProfileResetToSuggestedMappingCallback)
{ {
bulkLoadProfileResetToSuggestedMappingCallback(); bulkLoadProfileResetToSuggestedMappingCallback();
} }
@ -267,7 +265,6 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current
const bulkLoadProfile = currentMapping.toProfile(); const bulkLoadProfile = currentMapping.toProfile();
const mappingJson = JSON.stringify(bulkLoadProfile.profile); const mappingJson = JSON.stringify(bulkLoadProfile.profile);
formData.append("mappingJson", mappingJson); formData.append("mappingJson", mappingJson);
formData.append("isBulkEdit", isBulkEdit.toString());
if (isSaveAsAction || isRenameAction || currentSavedBulkLoadProfileRecord == null) if (isSaveAsAction || isRenameAction || currentSavedBulkLoadProfileRecord == null)
{ {
@ -392,7 +389,6 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current
return (savedBulkLoadProfiles); return (savedBulkLoadProfiles);
} }
const bulkAction = isBulkEdit ? "Edit" : "Load";
const hasStorePermission = metaData?.processes.has("storeSavedBulkLoadProfile"); const hasStorePermission = metaData?.processes.has("storeSavedBulkLoadProfile");
const hasDeletePermission = metaData?.processes.has("deleteSavedBulkLoadProfile"); const hasDeletePermission = metaData?.processes.has("deleteSavedBulkLoadProfile");
const hasQueryPermission = metaData?.processes.has("querySavedBulkLoadProfile"); const hasQueryPermission = metaData?.processes.has("querySavedBulkLoadProfile");
@ -432,15 +428,15 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current
PaperProps={{style: {maxHeight: "calc(100vh - 200px)", minWidth: menuWidth}}} PaperProps={{style: {maxHeight: "calc(100vh - 200px)", minWidth: menuWidth}}}
> >
{ {
<MenuItem sx={{width: menuWidth}} disabled style={{opacity: "initial"}}><b>Bulk {bulkAction} Profile Actions</b></MenuItem> <MenuItem sx={{width: menuWidth}} disabled style={{opacity: "initial"}}><b>Bulk Load Profile Actions</b></MenuItem>
} }
{ {
!allowSelectingProfile && !allowSelectingProfile &&
<MenuItem sx={{width: menuWidth}} disabled style={{opacity: "initial", whiteSpace: "wrap", display: "block"}}> <MenuItem sx={{width: menuWidth}} disabled style={{opacity: "initial", whiteSpace: "wrap", display: "block"}}>
{ {
currentSavedBulkLoadProfileRecord ? currentSavedBulkLoadProfileRecord ?
<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 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 {bulkAction.toLowerCase()} profile.<br /><br />You can save your 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>
} }
</MenuItem> </MenuItem>
} }
@ -460,7 +456,7 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current
} }
{ {
hasStorePermission && currentSavedBulkLoadProfileRecord != null && hasStorePermission && currentSavedBulkLoadProfileRecord != null &&
<Tooltip {...menuTooltipAttribs} title={notOwnerTooltipText ?? "Change the name for this saved bulk {bulkAction.toLowerCase()} profile."}> <Tooltip {...menuTooltipAttribs} title={notOwnerTooltipText ?? "Change the name for this saved bulk load profile."}>
<span> <span>
<MenuItem disabled={currentSavedBulkLoadProfileRecord === null || disabledBecauseNotOwner} onClick={() => handleDropdownOptionClick(RENAME_OPTION)}> <MenuItem disabled={currentSavedBulkLoadProfileRecord === null || disabledBecauseNotOwner} onClick={() => handleDropdownOptionClick(RENAME_OPTION)}>
<ListItemIcon><Icon>edit</Icon></ListItemIcon> <ListItemIcon><Icon>edit</Icon></ListItemIcon>
@ -471,7 +467,7 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current
} }
{ {
hasStorePermission && currentSavedBulkLoadProfileRecord != null && hasStorePermission && currentSavedBulkLoadProfileRecord != null &&
<Tooltip {...menuTooltipAttribs} title="Save a new copy this bulk {bulkAction.toLowerCase()} profile, with a different name, separate from the original."> <Tooltip {...menuTooltipAttribs} title="Save a new copy this bulk load profile, with a different name, separate from the original.">
<span> <span>
<MenuItem disabled={currentSavedBulkLoadProfileRecord === null} onClick={() => handleDropdownOptionClick(DUPLICATE_OPTION)}> <MenuItem disabled={currentSavedBulkLoadProfileRecord === null} onClick={() => handleDropdownOptionClick(DUPLICATE_OPTION)}>
<ListItemIcon><Icon>content_copy</Icon></ListItemIcon> <ListItemIcon><Icon>content_copy</Icon></ListItemIcon>
@ -482,7 +478,7 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current
} }
{ {
hasDeletePermission && currentSavedBulkLoadProfileRecord != null && hasDeletePermission && currentSavedBulkLoadProfileRecord != null &&
<Tooltip {...menuTooltipAttribs} title={notOwnerTooltipText ?? "Delete this saved bulk {bulkAction.toLowerCase()} profile."}> <Tooltip {...menuTooltipAttribs} title={notOwnerTooltipText ?? "Delete this saved bulk load profile."}>
<span> <span>
<MenuItem disabled={currentSavedBulkLoadProfileRecord === null || disabledBecauseNotOwner} onClick={() => handleDropdownOptionClick(DELETE_OPTION)}> <MenuItem disabled={currentSavedBulkLoadProfileRecord === null || disabledBecauseNotOwner} onClick={() => handleDropdownOptionClick(DELETE_OPTION)}>
<ListItemIcon><Icon>delete</Icon></ListItemIcon> <ListItemIcon><Icon>delete</Icon></ListItemIcon>
@ -493,11 +489,11 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current
} }
{ {
allowSelectingProfile && allowSelectingProfile &&
<Tooltip {...menuTooltipAttribs} title="Create a new blank bulk {bulkAction.toLowerCase()} profile for this table, removing all mappings."> <Tooltip {...menuTooltipAttribs} title="Create a new blank bulk load profile for this table, removing all mappings.">
<span> <span>
<MenuItem onClick={() => handleDropdownOptionClick(CLEAR_OPTION)}> <MenuItem onClick={() => handleDropdownOptionClick(CLEAR_OPTION)}>
<ListItemIcon><Icon>monitor</Icon></ListItemIcon> <ListItemIcon><Icon>monitor</Icon></ListItemIcon>
New Bulk {bulkAction} Profile New Bulk Load Profile
</MenuItem> </MenuItem>
</span> </span>
</Tooltip> </Tooltip>
@ -508,7 +504,7 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current
{ {
<Divider /> <Divider />
} }
<MenuItem disabled style={{"opacity": "initial"}}><b>Your Saved Bulk {bulkAction} Profiles</b></MenuItem> <MenuItem disabled style={{"opacity": "initial"}}><b>Your Saved Bulk Load Profiles</b></MenuItem>
{ {
yourSavedBulkLoadProfiles && yourSavedBulkLoadProfiles.length > 0 ? ( yourSavedBulkLoadProfiles && yourSavedBulkLoadProfiles.length > 0 ? (
yourSavedBulkLoadProfiles.map((record: QRecord, index: number) => yourSavedBulkLoadProfiles.map((record: QRecord, index: number) =>
@ -518,11 +514,11 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current
) )
) : ( ) : (
<MenuItem disabled sx={{opacity: "1 !important"}}> <MenuItem disabled sx={{opacity: "1 !important"}}>
<i>You do not have any saved bulk {bulkAction.toLowerCase()} profiles for this table.</i> <i>You do not have any saved bulk load profiles for this table.</i>
</MenuItem> </MenuItem>
) )
} }
<MenuItem disabled style={{"opacity": "initial"}}><b>Bulk {bulkAction} Profiles Shared with you</b></MenuItem> <MenuItem disabled style={{"opacity": "initial"}}><b>Bulk Load Profiles Shared with you</b></MenuItem>
{ {
bulkLoadProfilesSharedWithYou && bulkLoadProfilesSharedWithYou.length > 0 ? ( bulkLoadProfilesSharedWithYou && bulkLoadProfilesSharedWithYou.length > 0 ? (
bulkLoadProfilesSharedWithYou.map((record: QRecord, index: number) => bulkLoadProfilesSharedWithYou.map((record: QRecord, index: number) =>
@ -532,7 +528,7 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current
) )
) : ( ) : (
<MenuItem disabled sx={{opacity: "1 !important"}}> <MenuItem disabled sx={{opacity: "1 !important"}}>
<i>You do not have any bulk {bulkAction.toLowerCase()} profiles shared with you for this table.</i> <i>You do not have any bulk load profiles shared with you for this table.</i>
</MenuItem> </MenuItem>
) )
} }
@ -541,7 +537,7 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current
</Menu> </Menu>
); );
let buttonText = `Saved Bulk ${bulkAction} Profiles`; let buttonText = "Saved Bulk Load Profiles";
let buttonBackground = "none"; let buttonBackground = "none";
let buttonBorder = colors.grayLines.main; let buttonBorder = colors.grayLines.main;
let buttonColor = colors.gray.main; let buttonColor = colors.gray.main;
@ -643,13 +639,13 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current
<Tooltip {...tooltipMaxWidth("24rem")} sx={{cursor: "pointer"}} title={<> <Tooltip {...tooltipMaxWidth("24rem")} sx={{cursor: "pointer"}} title={<>
<b>Unsaved Mapping</b> <b>Unsaved Mapping</b>
<ul style={{padding: "0.5rem 1rem"}}> <ul style={{padding: "0.5rem 1rem"}}>
<li>You are not using a saved bulk {bulkAction.toLowerCase()} profile.</li> <li>You are not using a saved bulk load profile.</li>
{ {
/*bulkLoadProfileDiffs.map((s: string, i: number) => <li key={i}>{s}</li>)*/ /*bulkLoadProfileDiffs.map((s: string, i: number) => <li key={i}>{s}</li>)*/
} }
</ul> </ul>
</>}> </>}>
<Button disableRipple={true} sx={linkButtonStyle} onClick={() => handleDropdownOptionClick(SAVE_OPTION)}>Save Bulk {bulkAction} Profile As&hellip;</Button> <Button disableRipple={true} sx={linkButtonStyle} onClick={() => handleDropdownOptionClick(SAVE_OPTION)}>Save Bulk Load Profile As&hellip;</Button>
</Tooltip> </Tooltip>
{/* vertical rule */} {/* vertical rule */}
@ -720,20 +716,20 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current
{ {
currentSavedBulkLoadProfileRecord ? ( currentSavedBulkLoadProfileRecord ? (
isDeleteAction ? ( isDeleteAction ? (
<DialogTitle id="alert-dialog-title">Delete Bulk {bulkAction} Profile</DialogTitle> <DialogTitle id="alert-dialog-title">Delete Bulk Load Profile</DialogTitle>
) : ( ) : (
isSaveAsAction ? ( isSaveAsAction ? (
<DialogTitle id="alert-dialog-title">Save Bulk {bulkAction} Profile As</DialogTitle> <DialogTitle id="alert-dialog-title">Save Bulk Load Profile As</DialogTitle>
) : ( ) : (
isRenameAction ? ( isRenameAction ? (
<DialogTitle id="alert-dialog-title">Rename Bulk {bulkAction} Profile</DialogTitle> <DialogTitle id="alert-dialog-title">Rename Bulk Load Profile</DialogTitle>
) : ( ) : (
<DialogTitle id="alert-dialog-title">Update Existing Bulk {bulkAction} Profile</DialogTitle> <DialogTitle id="alert-dialog-title">Update Existing Bulk Load Profile</DialogTitle>
) )
) )
) )
) : ( ) : (
<DialogTitle id="alert-dialog-title">Save New Bulk {bulkAction} Profile</DialogTitle> <DialogTitle id="alert-dialog-title">Save New Bulk Load Profile</DialogTitle>
) )
} }
<DialogContent sx={{width: "500px"}}> <DialogContent sx={{width: "500px"}}>
@ -747,15 +743,15 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current
<Box> <Box>
{ {
isSaveAsAction ? ( isSaveAsAction ? (
<Box mb={3}>Enter a name for this new saved bulk {bulkAction.toLowerCase()} profile.</Box> <Box mb={3}>Enter a name for this new saved bulk load profile.</Box>
) : ( ) : (
<Box mb={3}>Enter a new name for this saved bulk {bulkAction.toLowerCase()} profile.</Box> <Box mb={3}>Enter a new name for this saved bulk load profile.</Box>
) )
} }
<TextField <TextField
autoFocus autoFocus
name="custom-delimiter-value" name="custom-delimiter-value"
placeholder={`Bulk ${bulkAction} Profile Name`} placeholder="Bulk Load Profile Name"
inputProps={{width: "100%", maxLength: 100}} inputProps={{width: "100%", maxLength: 100}}
value={savedBulkLoadProfileNameInputValue} value={savedBulkLoadProfileNameInputValue}
sx={{width: "100%"}} sx={{width: "100%"}}
@ -768,9 +764,9 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current
</Box> </Box>
) : ( ) : (
isDeleteAction ? ( isDeleteAction ? (
<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 delete 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> <Box>Are you sure you want to update the bulk load profile {`'${currentSavedBulkLoadProfileRecord?.values.get("label")}'`}?</Box>
) )
) )
} }

View File

@ -43,7 +43,6 @@ interface BulkLoadMappingFieldProps
removeFieldCallback?: () => void, removeFieldCallback?: () => void,
fileDescription: FileDescription, fileDescription: FileDescription,
forceParentUpdate?: () => void, forceParentUpdate?: () => void,
isBulkEdit?: boolean
} }
const xIconButtonSX = const xIconButtonSX =
@ -72,7 +71,7 @@ const qController = Client.getInstance();
/*************************************************************************** /***************************************************************************
** row for a single field on the bulk load mapping screen. ** row for a single field on the bulk load mapping screen.
***************************************************************************/ ***************************************************************************/
export default function BulkLoadFileMappingField({bulkLoadField, isRequired, removeFieldCallback, fileDescription, forceParentUpdate, isBulkEdit}: BulkLoadMappingFieldProps): JSX.Element export default function BulkLoadFileMappingField({bulkLoadField, isRequired, removeFieldCallback, fileDescription, forceParentUpdate}: BulkLoadMappingFieldProps): JSX.Element
{ {
const columnNames = fileDescription.getColumnNames(); const columnNames = fileDescription.getColumnNames();
@ -228,17 +227,6 @@ export default function BulkLoadFileMappingField({bulkLoadField, isRequired, rem
forceParentUpdate && forceParentUpdate(); forceParentUpdate && forceParentUpdate();
} }
/***************************************************************************
**
***************************************************************************/
function clearIfEmptyChanged(value: boolean)
{
bulkLoadField.clearIfEmpty = value;
forceParentUpdate && forceParentUpdate();
}
/*************************************************************************** /***************************************************************************
** **
***************************************************************************/ ***************************************************************************/
@ -325,11 +313,8 @@ export default function BulkLoadFileMappingField({bulkLoadField, isRequired, rem
<Box ml="1rem"> <Box ml="1rem">
{ {
valueType == "column" && <> valueType == "column" && <>
<Box display="flex" alignItems="center" sx={{height: "45px"}}> <Box>
<FormControlLabel value="mapValues" control={<Checkbox size="small" defaultChecked={bulkLoadField.doValueMapping} onChange={(event, checked) => mapValuesChanged(checked)} />} label={"Map values"} sx={{minWidth: "140px", whiteSpace: "nowrap"}} /> <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>
<Box fontSize={mainFontSize} mt="0.5rem"> <Box fontSize={mainFontSize} mt="0.5rem">
Preview Values: <span style={{color: "gray"}}>{(fileDescription.getPreviewValues(selectedColumn?.value) ?? [""]).join(", ")}</span> Preview Values: <span style={{color: "gray"}}>{(fileDescription.getPreviewValues(selectedColumn?.value) ?? [""]).join(", ")}</span>

View File

@ -33,7 +33,6 @@ interface BulkLoadMappingFieldsProps
bulkLoadMapping: BulkLoadMapping, bulkLoadMapping: BulkLoadMapping,
fileDescription: FileDescription, fileDescription: FileDescription,
forceParentUpdate?: () => void, forceParentUpdate?: () => void,
isBulkEdit?: boolean
} }
@ -44,7 +43,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. ** The section of the bulk load mapping screen with all the fields.
***************************************************************************/ ***************************************************************************/
export default function BulkLoadFileMappingFields({bulkLoadMapping, fileDescription, forceParentUpdate, isBulkEdit}: BulkLoadMappingFieldsProps): JSX.Element export default function BulkLoadFileMappingFields({bulkLoadMapping, fileDescription, forceParentUpdate}: BulkLoadMappingFieldsProps): JSX.Element
{ {
const [, forceUpdate] = useReducer((x) => x + 1, 0); const [, forceUpdate] = useReducer((x) => x + 1, 0);
@ -255,16 +254,11 @@ export default function BulkLoadFileMappingFields({bulkLoadMapping, fileDescript
return ( return (
<> <>
{isBulkEdit ? <h5>Key Fields</h5> : <h5>Required Fields</h5>} <h5>Required Fields</h5>
<Box pl={"1rem"}> <Box pl={"1rem"}>
{ {
bulkLoadMapping.requiredFields.length == 0 && bulkLoadMapping.requiredFields.length == 0 &&
(
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> <i style={{fontSize: "0.875rem"}}>There are no required fields in this table.</i>
)
} }
{bulkLoadMapping.requiredFields.map((bulkLoadField) => ( {bulkLoadMapping.requiredFields.map((bulkLoadField) => (
<BulkLoadFileMappingField <BulkLoadFileMappingField
@ -273,13 +267,12 @@ export default function BulkLoadFileMappingFields({bulkLoadMapping, fileDescript
bulkLoadField={bulkLoadField} bulkLoadField={bulkLoadField}
isRequired={true} isRequired={true}
forceParentUpdate={forceParentUpdate} forceParentUpdate={forceParentUpdate}
isBulkEdit={isBulkEdit}
/> />
))} ))}
</Box> </Box>
<Box mt="1rem"> <Box mt="1rem">
{isBulkEdit ? <h5>Fields To Update</h5> : <h5>Additional Fields</h5>} <h5>Additional Fields</h5>
<Box pl={"1rem"}> <Box pl={"1rem"}>
{bulkLoadMapping.additionalFields.map((bulkLoadField) => ( {bulkLoadMapping.additionalFields.map((bulkLoadField) => (
<BulkLoadFileMappingField <BulkLoadFileMappingField
@ -289,7 +282,6 @@ export default function BulkLoadFileMappingFields({bulkLoadMapping, fileDescript
isRequired={false} isRequired={false}
removeFieldCallback={() => removeField(bulkLoadField)} removeFieldCallback={() => removeField(bulkLoadField)}
forceParentUpdate={forceParentUpdate} forceParentUpdate={forceParentUpdate}
isBulkEdit={isBulkEdit}
/> />
))} ))}

View File

@ -36,18 +36,15 @@ import {useFormikContext} from "formik";
import colors from "qqq/assets/theme/base/colors"; import colors from "qqq/assets/theme/base/colors";
import {DynamicFormFieldLabel} from "qqq/components/forms/DynamicForm"; import {DynamicFormFieldLabel} from "qqq/components/forms/DynamicForm";
import QDynamicFormField from "qqq/components/forms/DynamicFormField"; import QDynamicFormField from "qqq/components/forms/DynamicFormField";
import DynamicFormUtils from "qqq/components/forms/DynamicFormUtils";
import MDTypography from "qqq/components/legacy/MDTypography"; import MDTypography from "qqq/components/legacy/MDTypography";
import HelpContent from "qqq/components/misc/HelpContent"; import HelpContent from "qqq/components/misc/HelpContent";
import SavedBulkLoadProfiles from "qqq/components/misc/SavedBulkLoadProfiles"; import SavedBulkLoadProfiles from "qqq/components/misc/SavedBulkLoadProfiles";
import BulkLoadFileMappingFields from "qqq/components/processes/BulkLoadFileMappingFields"; import BulkLoadFileMappingFields from "qqq/components/processes/BulkLoadFileMappingFields";
import {BulkLoadField, BulkLoadMapping, BulkLoadProfile, BulkLoadTableStructure, FileDescription, Wrapper} from "qqq/models/processes/BulkLoadModels"; import {BulkLoadField, BulkLoadMapping, BulkLoadProfile, BulkLoadTableStructure, FileDescription, Wrapper} from "qqq/models/processes/BulkLoadModels";
import {SubFormPreSubmitCallbackResultType} from "qqq/pages/processes/ProcessRun"; import {SubFormPreSubmitCallbackResultType} from "qqq/pages/processes/ProcessRun";
import Client from "qqq/utils/qqq/Client"; import React, {forwardRef, useImperativeHandle, useReducer, useState} from "react";
import React, {forwardRef, useEffect, useImperativeHandle, useReducer, useState} from "react";
import ProcessViewForm from "./ProcessViewForm"; import ProcessViewForm from "./ProcessViewForm";
const qController = Client.getInstance();
interface BulkLoadMappingFormProps interface BulkLoadMappingFormProps
{ {
@ -76,12 +73,13 @@ const BulkLoadFileMappingForm = forwardRef(({processValues, tableMetaData, metaD
const [suggestedBulkLoadProfile] = useState(processValues.suggestedBulkLoadProfile as BulkLoadProfile); const [suggestedBulkLoadProfile] = useState(processValues.suggestedBulkLoadProfile as BulkLoadProfile);
const [tableStructure] = useState(processValues.tableStructure as BulkLoadTableStructure); const [tableStructure] = useState(processValues.tableStructure as BulkLoadTableStructure);
const [bulkLoadMapping, setBulkLoadMapping] = useState(BulkLoadMapping.fromBulkLoadProfile(tableStructure, processValues.bulkLoadProfile, processMetaData.name)); const [bulkLoadMapping, setBulkLoadMapping] = useState(BulkLoadMapping.fromBulkLoadProfile(tableStructure, processValues.bulkLoadProfile));
const [wrappedBulkLoadMapping] = useState(new Wrapper<BulkLoadMapping>(bulkLoadMapping)); const [wrappedBulkLoadMapping] = useState(new Wrapper<BulkLoadMapping>(bulkLoadMapping));
const [fileDescription] = useState(new FileDescription(processValues.headerValues, processValues.headerLetters, processValues.bodyValuesPreview)); const [fileDescription] = useState(new FileDescription(processValues.headerValues, processValues.headerLetters, processValues.bodyValuesPreview));
fileDescription.setHasHeaderRow(bulkLoadMapping.hasHeaderRow); fileDescription.setHasHeaderRow(bulkLoadMapping.hasHeaderRow);
const [, forceUpdate] = useReducer((x) => x + 1, 0); const [, forceUpdate] = useReducer((x) => x + 1, 0);
///////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////
@ -116,8 +114,6 @@ const BulkLoadFileMappingForm = forwardRef(({processValues, tableMetaData, metaD
values["savedBulkLoadProfileId"] = wrappedCurrentSavedBulkLoadProfile.get()?.values?.get("id"); values["savedBulkLoadProfileId"] = wrappedCurrentSavedBulkLoadProfile.get()?.values?.get("id");
values["layout"] = wrappedBulkLoadMapping.get().layout; values["layout"] = wrappedBulkLoadMapping.get().layout;
values["hasHeaderRow"] = wrappedBulkLoadMapping.get().hasHeaderRow; values["hasHeaderRow"] = wrappedBulkLoadMapping.get().hasHeaderRow;
values["isBulkEdit"] = wrappedBulkLoadMapping.get().isBulkEdit;
values["keyFields"] = wrappedBulkLoadMapping.get().keyFields;
let haveLocalErrors = false; let haveLocalErrors = false;
const fieldErrors: { [fieldName: string]: string } = {}; const fieldErrors: { [fieldName: string]: string } = {};
@ -134,14 +130,7 @@ const BulkLoadFileMappingForm = forwardRef(({processValues, tableMetaData, metaD
} }
setFieldErrors(fieldErrors); setFieldErrors(fieldErrors);
if (values["isBulkEdit"] && (values["keyFields"] == null || values["keyFields"] == undefined)) if(wrappedBulkLoadMapping.get().requiredFields.length == 0 && wrappedBulkLoadMapping.get().additionalFields.length == 0)
{
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."); setNoMappedFieldsError("You must have at least 1 field.");
haveLocalErrors = true; haveLocalErrors = true;
@ -152,7 +141,7 @@ const BulkLoadFileMappingForm = forwardRef(({processValues, tableMetaData, metaD
setNoMappedFieldsError(null); setNoMappedFieldsError(null);
} }
if (haveProfileErrors) if(haveProfileErrors)
{ {
setTimeout(() => setTimeout(() =>
{ {
@ -193,7 +182,7 @@ const BulkLoadFileMappingForm = forwardRef(({processValues, tableMetaData, metaD
***************************************************************************/ ***************************************************************************/
function bulkLoadProfileResetToSuggestedMappingCallback() function bulkLoadProfileResetToSuggestedMappingCallback()
{ {
handleNewBulkLoadMapping(BulkLoadMapping.fromBulkLoadProfile(processValues.tableStructure, suggestedBulkLoadProfile, processValues.name)); handleNewBulkLoadMapping(BulkLoadMapping.fromBulkLoadProfile(processValues.tableStructure, suggestedBulkLoadProfile));
} }
@ -212,8 +201,6 @@ const BulkLoadFileMappingForm = forwardRef(({processValues, tableMetaData, metaD
setBulkLoadMapping(newBulkLoadMapping); setBulkLoadMapping(newBulkLoadMapping);
wrappedBulkLoadMapping.set(newBulkLoadMapping); wrappedBulkLoadMapping.set(newBulkLoadMapping);
setFieldValue("isBulkEdit", newBulkLoadMapping.isBulkEdit);
setFieldValue("keyFields", newBulkLoadMapping.keyFields);
setFieldValue("hasHeaderRow", newBulkLoadMapping.hasHeaderRow); setFieldValue("hasHeaderRow", newBulkLoadMapping.hasHeaderRow);
setFieldValue("layout", newBulkLoadMapping.layout); setFieldValue("layout", newBulkLoadMapping.layout);
@ -241,13 +228,10 @@ const BulkLoadFileMappingForm = forwardRef(({processValues, tableMetaData, metaD
bulkLoadProfileOnChangeCallback={bulkLoadProfileOnChangeCallback} bulkLoadProfileOnChangeCallback={bulkLoadProfileOnChangeCallback}
bulkLoadProfileResetToSuggestedMappingCallback={bulkLoadProfileResetToSuggestedMappingCallback} bulkLoadProfileResetToSuggestedMappingCallback={bulkLoadProfileResetToSuggestedMappingCallback}
fileDescription={fileDescription} fileDescription={fileDescription}
isBulkEdit={processValues.isBulkEdit}
/> />
</Box> </Box>
<BulkLoadMappingHeader <BulkLoadMappingHeader
tableMetaData={tableMetaData}
isBulkEdit={processValues.isBulkEdit}
key={rerenderHeader} key={rerenderHeader}
bulkLoadMapping={bulkLoadMapping} bulkLoadMapping={bulkLoadMapping}
fileDescription={fileDescription} fileDescription={fileDescription}
@ -261,7 +245,6 @@ const BulkLoadFileMappingForm = forwardRef(({processValues, tableMetaData, metaD
<Box mt="2rem"> <Box mt="2rem">
<BulkLoadFileMappingFields <BulkLoadFileMappingFields
isBulkEdit={processValues.isBulkEdit}
bulkLoadMapping={bulkLoadMapping} bulkLoadMapping={bulkLoadMapping}
fileDescription={fileDescription} fileDescription={fileDescription}
forceParentUpdate={() => forceParentUpdate={() =>
@ -284,7 +267,6 @@ export default BulkLoadFileMappingForm;
interface BulkLoadMappingHeaderProps interface BulkLoadMappingHeaderProps
{ {
isBulkEdit?: boolean,
fileDescription: FileDescription, fileDescription: FileDescription,
fileName: string, fileName: string,
bulkLoadMapping?: BulkLoadMapping, bulkLoadMapping?: BulkLoadMapping,
@ -293,16 +275,13 @@ interface BulkLoadMappingHeaderProps
forceParentUpdate?: () => void, forceParentUpdate?: () => void,
frontendStep: QFrontendStepMetaData, frontendStep: QFrontendStepMetaData,
processMetaData: QProcessMetaData, processMetaData: QProcessMetaData,
tableMetaData: QTableMetaData,
} }
/*************************************************************************** /***************************************************************************
** private subcomponent - the header section of the bulk load file mapping screen. ** private subcomponent - the header section of the bulk load file mapping screen.
***************************************************************************/ ***************************************************************************/
function BulkLoadMappingHeader({isBulkEdit, fileDescription, fileName, bulkLoadMapping, fieldErrors, tableStructure, forceParentUpdate, frontendStep, processMetaData, tableMetaData}: BulkLoadMappingHeaderProps): JSX.Element function BulkLoadMappingHeader({fileDescription, fileName, bulkLoadMapping, fieldErrors, tableStructure, forceParentUpdate, frontendStep, processMetaData}: BulkLoadMappingHeaderProps): JSX.Element
{ {
const [dynamicField, setDynamicField] = useState(null);
const viewFields = [ const viewFields = [
new QFieldMetaData({name: "fileName", label: "File Name", type: "STRING"}), new QFieldMetaData({name: "fileName", label: "File Name", type: "STRING"}),
new QFieldMetaData({name: "fileDetails", label: "File Details", type: "STRING"}), new QFieldMetaData({name: "fileDetails", label: "File Details", type: "STRING"}),
@ -328,36 +307,6 @@ function BulkLoadMappingHeader({isBulkEdit, fileDescription, fileName, bulkLoadM
const selectedLayout = layoutOptions.filter(o => o.id == bulkLoadMapping.layout)[0] ?? null; 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)]);
/*************************************************************************** /***************************************************************************
** **
***************************************************************************/ ***************************************************************************/
@ -382,61 +331,6 @@ function BulkLoadMappingHeader({isBulkEdit, fileDescription, fileName, bulkLoadM
forceParentUpdate(); 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();
}
/*************************************************************************** /***************************************************************************
** **
***************************************************************************/ ***************************************************************************/
@ -475,9 +369,6 @@ function BulkLoadMappingHeader({isBulkEdit, fileDescription, fileName, bulkLoadM
{getFormattedHelpContent("hasHeaderRow")} {getFormattedHelpContent("hasHeaderRow")}
</Grid> </Grid>
<Grid item xs={12} md={6}> <Grid item xs={12} md={6}>
{
!isBulkEdit ? (
<>
<DynamicFormFieldLabel name={"layout"} label={"File Layout *"} /> <DynamicFormFieldLabel name={"layout"} label={"File Layout *"} />
<Autocomplete <Autocomplete
id={"layout"} id={"layout"}
@ -499,26 +390,6 @@ function BulkLoadMappingHeader({isBulkEdit, fileDescription, fileName, bulkLoadM
</MDTypography> </MDTypography>
} }
{getFormattedHelpContent("layout")} {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")}
</>
}
</>
)
}
</Grid> </Grid>
</Grid> </Grid>
</Box> </Box>
@ -619,12 +490,12 @@ function BulkLoadMappingFilePreview({fileDescription, bulkLoadMapping}: BulkLoad
const fields = bulkLoadMapping.getFieldsForColumnIndex(index); const fields = bulkLoadMapping.getFieldsForColumnIndex(index);
const count = fields.length; const count = fields.length;
let dupeWarning = <></>; let dupeWarning = <></>
if (fileDescription.hasHeaderRow && fileDescription.duplicateHeaderIndexes[index]) 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}> 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> <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)}}> return (<td key={letter} style={{textAlign: "center", color: getHeaderColor(count), cursor: getCursor(count)}}>
@ -657,24 +528,24 @@ function BulkLoadMappingFilePreview({fileDescription, bulkLoadMapping}: BulkLoad
const count = fields.length; const count = fields.length;
const tdStyle = {color: getHeaderColor(count), cursor: getCursor(count), backgroundColor: ""}; const tdStyle = {color: getHeaderColor(count), cursor: getCursor(count), backgroundColor: ""};
if (fileDescription.hasHeaderRow) if(fileDescription.hasHeaderRow)
{ {
tdStyle.backgroundColor = "#ebebeb"; tdStyle.backgroundColor = "#ebebeb";
if (count > 0) if(count > 0)
{ {
return <td key={value} style={tdStyle}> return <td key={value} style={tdStyle}>
<Tooltip title={getColumnTooltip(fields)} placement="top" enterDelay={500}><Box>{value}</Box></Tooltip> <Tooltip title={getColumnTooltip(fields)} placement="top" enterDelay={500}><Box>{value}</Box></Tooltip>
</td>; </td>
} }
else else
{ {
return <td key={value} style={tdStyle}>{value}</td>; return <td key={value} style={tdStyle}>{value}</td>
} }
} }
else else
{ {
return <td key={value} style={tdStyle}>{value}</td>; return <td key={value} style={tdStyle}>{value}</td>
} }
} }
)} )}

View File

@ -43,12 +43,12 @@ interface BulkLoadValueMappingFormProps
const BulkLoadProfileForm = forwardRef(({processValues, tableMetaData, metaData}: BulkLoadValueMappingFormProps, ref) => const BulkLoadProfileForm = forwardRef(({processValues, tableMetaData, metaData}: BulkLoadValueMappingFormProps, ref) =>
{ {
const savedBulkLoadProfileRecordProcessValue = processValues.savedBulkLoadProfileRecord; 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 [tableStructure] = useState(processValues.tableStructure as BulkLoadTableStructure);
const [bulkLoadProfile, setBulkLoadProfile] = useState(processValues.bulkLoadProfile as BulkLoadProfile); 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 [wrappedCurrentSavedBulkLoadProfile] = useState(new Wrapper<QRecord>(savedBulkLoadProfileRecord));
const [fileDescription] = useState(new FileDescription(processValues.headerValues, processValues.headerLetters, processValues.bodyValuesPreview)); const [fileDescription] = useState(new FileDescription(processValues.headerValues, processValues.headerLetters, processValues.bodyValuesPreview));
@ -93,7 +93,6 @@ const BulkLoadProfileForm = forwardRef(({processValues, tableMetaData, metaData}
allowSelectingProfile={false} allowSelectingProfile={false}
fileDescription={fileDescription} fileDescription={fileDescription}
bulkLoadProfileOnChangeCallback={bulkLoadProfileOnChangeCallback} bulkLoadProfileOnChangeCallback={bulkLoadProfileOnChangeCallback}
isBulkEdit={processValues.isBulkEdit}
/> />
</Box> </Box>

View File

@ -75,7 +75,7 @@ const BulkLoadValueMappingForm = forwardRef(({processValues, setActiveStepLabel,
*******************************************************************************/ *******************************************************************************/
function initializeCurrentBulkLoadMapping(): BulkLoadMapping function initializeCurrentBulkLoadMapping(): BulkLoadMapping
{ {
const bulkLoadMapping = BulkLoadMapping.fromBulkLoadProfile(tableStructure, bulkLoadProfile, processValues.name); const bulkLoadMapping = BulkLoadMapping.fromBulkLoadProfile(tableStructure, bulkLoadProfile);
if (!bulkLoadMapping.valueMappings[fieldFullName]) if (!bulkLoadMapping.valueMappings[fieldFullName])
{ {
@ -155,7 +155,7 @@ const BulkLoadValueMappingForm = forwardRef(({processValues, setActiveStepLabel,
function mappedValueChanged(fileValue: string, newValue: any) function mappedValueChanged(fileValue: string, newValue: any)
{ {
valueErrors[fileValue] = null; valueErrors[fileValue] = null;
if (newValue == null) if(newValue == null)
{ {
delete currentMapping.valueMappings[fieldFullName][fileValue]; delete currentMapping.valueMappings[fieldFullName][fileValue];
} }
@ -195,7 +195,6 @@ const BulkLoadValueMappingForm = forwardRef(({processValues, setActiveStepLabel,
allowSelectingProfile={false} allowSelectingProfile={false}
bulkLoadProfileOnChangeCallback={bulkLoadProfileOnChangeCallback} bulkLoadProfileOnChangeCallback={bulkLoadProfileOnChangeCallback}
fileDescription={fileDescription} fileDescription={fileDescription}
isBulkEdit={processValues.isBulkEdit}
/> />
</Box> </Box>

View File

@ -342,7 +342,7 @@ function FilterCriteriaPaster({table, field, type, onSave}: Props): JSX.Element
if (type === "number") if (type === "number")
{ {
let suffix = invalidCount === 1 ? " value is not a number" : " values are not numbers"; let suffix = invalidCount === 1 ? " value is not a number" : " values are not numbers";
setErrorText(invalidCount + suffix + " and will not be added to the filter"); setErrorText(invalidCount + suffix + "numbers and will not be added to the filter");
} }
else if (type === "pvs") else if (type === "pvs")
{ {
@ -363,7 +363,7 @@ function FilterCriteriaPaster({table, field, type, onSave}: Props): JSX.Element
return ( return (
<Box> <Box>
<Tooltip title="Quickly add many values to your filter by pasting them from a spreadsheet or any other data source."> <Tooltip title="Quickly add many values to your filter by pasting them from a spreadsheet or any other data source.">
<Icon className="criteriaPasterButton" onClick={handlePasteClick} fontSize="small" color="info" sx={{mx: 0.25, cursor: "pointer"}}>paste_content</Icon> <Icon onClick={handlePasteClick} fontSize="small" color="info" sx={{mx: 0.25, cursor: "pointer"}}>paste_content</Icon>
</Tooltip> </Tooltip>
{ {
pasteModalIsOpen && pasteModalIsOpen &&
@ -391,7 +391,6 @@ 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}}> <Grid item pr={3} xs={6} lg={6} sx={{width: "100%", display: "flex", flexDirection: "column", flexGrow: 1}}>
<FormControl sx={{m: 1, width: "100%"}}> <FormControl sx={{m: 1, width: "100%"}}>
<TextField <TextField
className="criteriaPasterTextArea"
id="outlined-multiline-static" id="outlined-multiline-static"
label="PASTE TEXT" label="PASTE TEXT"
multiline multiline

View File

@ -29,9 +29,9 @@ import Icon from "@mui/material/Icon";
import ListItemIcon from "@mui/material/ListItemIcon"; import ListItemIcon from "@mui/material/ListItemIcon";
import Menu from "@mui/material/Menu"; import Menu from "@mui/material/Menu";
import MenuItem from "@mui/material/MenuItem"; import MenuItem from "@mui/material/MenuItem";
import {QActionsMenuButton} from "qqq/components/buttons/DefaultButtons";
import React, {useState} from "react"; import React, {useState} from "react";
import {useNavigate} from "react-router-dom"; import {useNavigate} from "react-router-dom";
import {QActionsMenuButton} from "qqq/components/buttons/DefaultButtons";
interface QueryScreenActionMenuProps interface QueryScreenActionMenuProps
{ {
@ -40,28 +40,28 @@ interface QueryScreenActionMenuProps
tableProcesses: QProcessMetaData[]; tableProcesses: QProcessMetaData[];
bulkLoadClicked: () => void; bulkLoadClicked: () => void;
bulkEditClicked: () => void; bulkEditClicked: () => void;
bulkEditWithFileClicked: () => void;
bulkDeleteClicked: () => void; bulkDeleteClicked: () => void;
processClicked: (process: QProcessMetaData) => void; processClicked: (process: QProcessMetaData) => void;
} }
QueryScreenActionMenu.defaultProps = {}; QueryScreenActionMenu.defaultProps = {
};
export default function QueryScreenActionMenu({metaData, tableMetaData, tableProcesses, bulkLoadClicked, bulkEditClicked, bulkEditWithFileClicked, bulkDeleteClicked, processClicked}: QueryScreenActionMenuProps): JSX.Element export default function QueryScreenActionMenu({metaData, tableMetaData, tableProcesses, bulkLoadClicked, bulkEditClicked, bulkDeleteClicked, processClicked}: QueryScreenActionMenuProps): JSX.Element
{ {
const [anchorElement, setAnchorElement] = useState(null); const [anchorElement, setAnchorElement] = useState(null)
const navigate = useNavigate(); const navigate = useNavigate();
const openActionsMenu = (event: any) => const openActionsMenu = (event: any) =>
{ {
setAnchorElement(event.currentTarget); setAnchorElement(event.currentTarget);
}; }
const closeActionsMenu = () => const closeActionsMenu = () =>
{ {
setAnchorElement(null); setAnchorElement(null);
}; }
const pushDividerIfNeeded = (menuItems: JSX.Element[]) => const pushDividerIfNeeded = (menuItems: JSX.Element[]) =>
{ {
@ -75,7 +75,7 @@ export default function QueryScreenActionMenu({metaData, tableMetaData, tablePro
{ {
closeActionsMenu(); closeActionsMenu();
handler(); handler();
}; }
const menuItems: JSX.Element[] = []; const menuItems: JSX.Element[] = [];
if (tableMetaData.capabilities.has(Capability.TABLE_INSERT) && tableMetaData.insertPermission) if (tableMetaData.capabilities.has(Capability.TABLE_INSERT) && tableMetaData.insertPermission)
@ -85,7 +85,6 @@ export default function QueryScreenActionMenu({metaData, tableMetaData, tablePro
if (tableMetaData.capabilities.has(Capability.TABLE_UPDATE) && tableMetaData.editPermission) 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="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) if (tableMetaData.capabilities.has(Capability.TABLE_DELETE) && tableMetaData.deletePermission)
{ {
@ -131,5 +130,5 @@ export default function QueryScreenActionMenu({metaData, tableMetaData, tablePro
{menuItems} {menuItems}
</Menu> </Menu>
</> </>
); )
} }

View File

@ -39,7 +39,6 @@ export class BulkLoadField
headerName?: string = null; headerName?: string = null;
defaultValue?: any = null; defaultValue?: any = null;
doValueMapping: boolean = false; doValueMapping: boolean = false;
clearIfEmpty?: boolean = false;
wideLayoutIndexPath: number[] = []; wideLayoutIndexPath: number[] = [];
@ -52,7 +51,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, clearIfEmpty?: boolean) constructor(field: QFieldMetaData, tableStructure: BulkLoadTableStructure, valueType: ValueType = "column", columnIndex?: number, headerName?: string, defaultValue?: any, doValueMapping?: boolean, wideLayoutIndexPath: number[] = [], error: string = null, warning: string = null)
{ {
this.field = field; this.field = field;
this.tableStructure = tableStructure; this.tableStructure = tableStructure;
@ -65,7 +64,6 @@ export class BulkLoadField
this.error = error; this.error = error;
this.warning = warning; this.warning = warning;
this.key = new Date().getTime().toString(); this.key = new Date().getTime().toString();
this.clearIfEmpty = clearIfEmpty ?? false;
} }
@ -74,7 +72,7 @@ export class BulkLoadField
***************************************************************************/ ***************************************************************************/
public static clone(source: BulkLoadField): 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, source.clearIfEmpty)); return (new BulkLoadField(source.field, source.tableStructure, source.valueType, source.columnIndex, source.headerName, source.defaultValue, source.doValueMapping, source.wideLayoutIndexPath, source.error, source.warning));
} }
@ -175,9 +173,6 @@ export interface BulkLoadTableStructure
associationPath: string; associationPath: string;
fields: QFieldMetaData[]; fields: QFieldMetaData[];
associations: BulkLoadTableStructure[]; associations: BulkLoadTableStructure[];
isBulkEdit: boolean;
possibleKeyFields: string[];
keyFields?: string;
} }
@ -198,8 +193,6 @@ export class BulkLoadMapping
valueMappings: { [fieldName: string]: { [fileValue: string]: any } } = {}; valueMappings: { [fieldName: string]: { [fileValue: string]: any } } = {};
isBulkEdit: boolean;
keyFields: string;
hasHeaderRow: boolean; hasHeaderRow: boolean;
layout: string; layout: string;
@ -218,8 +211,6 @@ export class BulkLoadMapping
} }
} }
this.isBulkEdit = tableStructure.isBulkEdit;
this.keyFields = tableStructure.keyFields;
this.hasHeaderRow = true; this.hasHeaderRow = true;
} }
@ -227,13 +218,11 @@ export class BulkLoadMapping
/*************************************************************************** /***************************************************************************
** **
***************************************************************************/ ***************************************************************************/
public processTableStructure(tableStructure: BulkLoadTableStructure) private processTableStructure(tableStructure: BulkLoadTableStructure)
{ {
const prefix = tableStructure.isMain ? "" : tableStructure.associationPath; const prefix = tableStructure.isMain ? "" : tableStructure.associationPath;
this.fieldsByTablePrefix[prefix] = {}; this.fieldsByTablePrefix[prefix] = {};
this.tablesByPath[prefix] = tableStructure; this.tablesByPath[prefix] = tableStructure;
this.isBulkEdit = tableStructure.isBulkEdit;
this.keyFields = tableStructure.keyFields;
for (let field of tableStructure.fields) for (let field of tableStructure.fields)
{ {
@ -244,27 +233,6 @@ export class BulkLoadMapping
this.fields[qualifiedName] = bulkLoadField; this.fields[qualifiedName] = bulkLoadField;
this.fieldsByTablePrefix[prefix][qualifiedName] = bulkLoadField; this.fieldsByTablePrefix[prefix][qualifiedName] = bulkLoadField;
if (this.isBulkEdit)
{
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
{
if (tableStructure.isMain && field.isRequired) if (tableStructure.isMain && field.isRequired)
{ {
this.requiredFields.push(bulkLoadField); this.requiredFields.push(bulkLoadField);
@ -275,7 +243,6 @@ export class BulkLoadMapping
} }
} }
} }
}
for (let associatedTableStructure of tableStructure.associations ?? []) for (let associatedTableStructure of tableStructure.associations ?? [])
{ {
@ -299,16 +266,14 @@ export class BulkLoadMapping
** take a saved bulk load profile - and convert it into a working bulkLoadMapping ** take a saved bulk load profile - and convert it into a working bulkLoadMapping
** for the frontend to use! ** for the frontend to use!
***************************************************************************/ ***************************************************************************/
public static fromBulkLoadProfile(tableStructure: BulkLoadTableStructure, bulkLoadProfile: BulkLoadProfile, processName?: string): BulkLoadMapping public static fromBulkLoadProfile(tableStructure: BulkLoadTableStructure, bulkLoadProfile: BulkLoadProfile): BulkLoadMapping
{ {
const bulkLoadMapping = new BulkLoadMapping(tableStructure); const bulkLoadMapping = new BulkLoadMapping(tableStructure);
if (bulkLoadProfile.version == "v1") if (bulkLoadProfile.version == "v1")
{ {
bulkLoadMapping.isBulkEdit = bulkLoadProfile.isBulkEdit;
bulkLoadMapping.hasHeaderRow = bulkLoadProfile.hasHeaderRow; bulkLoadMapping.hasHeaderRow = bulkLoadProfile.hasHeaderRow;
bulkLoadMapping.layout = bulkLoadProfile.layout; 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, // // function to get a bulkLoadMapping field by its (full) name - whether that's in the required fields list, //
@ -357,7 +322,6 @@ export class BulkLoadMapping
{ {
bulkLoadField.valueType = "column"; bulkLoadField.valueType = "column";
bulkLoadField.doValueMapping = bulkLoadProfileField.doValueMapping; bulkLoadField.doValueMapping = bulkLoadProfileField.doValueMapping;
bulkLoadField.clearIfEmpty = bulkLoadProfileField.clearIfEmpty;
bulkLoadField.headerName = bulkLoadProfileField.headerName; bulkLoadField.headerName = bulkLoadProfileField.headerName;
bulkLoadField.columnIndex = bulkLoadProfileField.columnIndex; bulkLoadField.columnIndex = bulkLoadProfileField.columnIndex;
@ -380,29 +344,6 @@ 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); return (bulkLoadMapping);
} }
else else
@ -424,8 +365,6 @@ export class BulkLoadMapping
profile.version = "v1"; profile.version = "v1";
profile.hasHeaderRow = this.hasHeaderRow; profile.hasHeaderRow = this.hasHeaderRow;
profile.layout = this.layout; profile.layout = this.layout;
profile.isBulkEdit = this.isBulkEdit;
profile.keyFields = this.keyFields;
for (let bulkLoadField of [...this.requiredFields, ...this.additionalFields]) for (let bulkLoadField of [...this.requiredFields, ...this.additionalFields])
{ {
@ -445,7 +384,7 @@ export class BulkLoadMapping
} }
else else
{ {
const field: BulkLoadProfileField = {fieldName: fullFieldName, columnIndex: bulkLoadField.columnIndex, headerName: bulkLoadField.headerName, doValueMapping: bulkLoadField.doValueMapping, clearIfEmpty: bulkLoadField.clearIfEmpty}; const field: BulkLoadProfileField = {fieldName: fullFieldName, columnIndex: bulkLoadField.columnIndex, headerName: bulkLoadField.headerName, doValueMapping: bulkLoadField.doValueMapping};
if (this.valueMappings[fullFieldName]) if (this.valueMappings[fullFieldName])
{ {
@ -637,16 +576,6 @@ export class BulkLoadMapping
return (rs); return (rs);
} }
/***************************************************************************
**
***************************************************************************/
public handleChangeToKeyFields(newKeyFields: any)
{
this.keyFields = newKeyFields;
}
/*************************************************************************** /***************************************************************************
** **
***************************************************************************/ ***************************************************************************/
@ -671,7 +600,7 @@ export class BulkLoadMapping
{ {
const newField = BulkLoadField.clone(field); const newField = BulkLoadField.clone(field);
newField.columnIndex = null; 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); newRequiredFields.push(newField);
anyChangesToRequiredFields = true; anyChangesToRequiredFields = true;
} }
@ -687,7 +616,7 @@ export class BulkLoadMapping
{ {
const newField = BulkLoadField.clone(field); const newField = BulkLoadField.clone(field);
newField.columnIndex = null; 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); newAdditionalFields.push(newField);
anyChangesToAdditionalFields = true; anyChangesToAdditionalFields = true;
} }
@ -869,8 +798,6 @@ export class BulkLoadProfile
fieldList: BulkLoadProfileField[] = []; fieldList: BulkLoadProfileField[] = [];
hasHeaderRow: boolean; hasHeaderRow: boolean;
layout: string; layout: string;
isBulkEdit: boolean;
keyFields: string;
} }
type BulkLoadProfileField = type BulkLoadProfileField =
@ -880,7 +807,6 @@ type BulkLoadProfileField =
headerName?: string, headerName?: string,
defaultValue?: any, defaultValue?: any,
doValueMapping?: boolean, doValueMapping?: boolean,
clearIfEmpty?: boolean,
valueMappings?: { [fileValue: string]: any } valueMappings?: { [fileValue: string]: any }
}; };

View File

@ -1557,7 +1557,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
/******************************************************************************* /*******************************************************************************
** function to open one of the bulk (insert/edit/delete) processes. ** function to open one of the bulk (insert/edit/delete) processes.
*******************************************************************************/ *******************************************************************************/
const openBulkProcess = (processNamePart: "Insert" | "Edit" | "Delete" | "EditWithFile", processLabelPart: "Load" | "Edit" | "Delete" | "Edit With File") => const openBulkProcess = (processNamePart: "Insert" | "Edit" | "Delete", processLabelPart: "Load" | "Edit" | "Delete") =>
{ {
const processList = allTableProcesses.filter(p => p.name.endsWith(`.bulk${processNamePart}`)); const processList = allTableProcesses.filter(p => p.name.endsWith(`.bulk${processNamePart}`));
if (processList.length > 0) if (processList.length > 0)
@ -1593,15 +1593,6 @@ 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 ** Event handler for the bulk-delete process being selected
*******************************************************************************/ *******************************************************************************/
@ -2870,7 +2861,6 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
tableProcesses={tableProcesses} tableProcesses={tableProcesses}
bulkLoadClicked={bulkLoadClicked} bulkLoadClicked={bulkLoadClicked}
bulkEditClicked={bulkEditClicked} bulkEditClicked={bulkEditClicked}
bulkEditWithFileClicked={bulkEditWithFileClicked}
bulkDeleteClicked={bulkDeleteClicked} bulkDeleteClicked={bulkDeleteClicked}
processClicked={processClicked} processClicked={processClicked}
/> />

View File

@ -103,30 +103,6 @@ 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));
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/

View File

@ -22,16 +22,13 @@
package com.kingsrook.qqq.frontend.materialdashboard.selenium.tests.query; package com.kingsrook.qqq.frontend.materialdashboard.selenium.tests.query;
import java.util.HashSet;
import java.util.List; 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.QBaseSeleniumTest;
import com.kingsrook.qqq.frontend.materialdashboard.selenium.lib.QQQMaterialDashboardSelectors; 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.QueryScreenLib;
import com.kingsrook.qqq.frontend.materialdashboard.selenium.lib.javalin.CapturedContext; import com.kingsrook.qqq.frontend.materialdashboard.selenium.lib.javalin.CapturedContext;
import com.kingsrook.qqq.frontend.materialdashboard.selenium.lib.javalin.QSeleniumJavalin; import com.kingsrook.qqq.frontend.materialdashboard.selenium.lib.javalin.QSeleniumJavalin;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.openqa.selenium.WebElement;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@ -203,204 +200,6 @@ 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");
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
@ -474,18 +273,4 @@ public class QueryScreenTest extends QBaseSeleniumTest
queryScreenLib.clickAdvancedFilterClearIcon(); 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);
}
} }