Merged feature/CE-798-quick-filters into integration/sprint-35

This commit is contained in:
2024-02-08 20:12:41 -06:00
17 changed files with 461 additions and 234 deletions

View File

@ -251,7 +251,7 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
{
pageHeader &&
<Box display="flex" justifyContent="space-between">
<MDTypography pt={1} textTransform="capitalize" variant="h3" color={light ? "white" : "dark"} noWrap>
<MDTypography pb="0.5rem" textTransform="capitalize" variant="h3" color={light ? "white" : "dark"} noWrap>
{pageHeader}
</MDTypography>
</Box>

View File

@ -405,7 +405,7 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
</Tooltip>
}
{
hasStorePermission &&
hasStorePermission && currentSavedView != null &&
<Tooltip {...menuTooltipAttribs} title="Change the name for this saved view.">
<MenuItem disabled={currentSavedView === null} onClick={() => handleDropdownOptionClick(RENAME_OPTION)}>
<ListItemIcon><Icon>edit</Icon></ListItemIcon>
@ -414,16 +414,16 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
</Tooltip>
}
{
hasStorePermission &&
<Tooltip {...menuTooltipAttribs} title="Make a copy this saved view, with a different name, separate from the original.">
hasStorePermission && currentSavedView != null &&
<Tooltip {...menuTooltipAttribs} title="Save a new copy this view, with a different name, separate from the original.">
<MenuItem disabled={currentSavedView === null} onClick={() => handleDropdownOptionClick(DUPLICATE_OPTION)}>
<ListItemIcon><Icon>content_copy</Icon></ListItemIcon>
Duplicate...
Save As...
</MenuItem>
</Tooltip>
}
{
hasDeletePermission &&
hasStorePermission && currentSavedView != null &&
<Tooltip {...menuTooltipAttribs} title="Delete this saved view.">
<MenuItem disabled={currentSavedView === null} onClick={() => handleDropdownOptionClick(DELETE_OPTION)}>
<ListItemIcon><Icon>delete</Icon></ListItemIcon>
@ -462,15 +462,6 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
let buttonBorder = colors.grayLines.main;
let buttonColor = colors.gray.main;
if(loadingSavedView)
{
buttonText = "Loading...";
}
else if(currentSavedView)
{
buttonText = currentSavedView.values.get("label")
}
if(currentSavedView)
{
if (viewIsModified)

View File

@ -99,6 +99,7 @@ const BasicAndAdvancedQueryControls = forwardRef((props: BasicAndAdvancedQueryCo
const [addQuickFilterMenu, setAddQuickFilterMenu] = useState(null)
const [addQuickFilterOpenCounter, setAddQuickFilterOpenCounter] = useState(0);
const [showClearFiltersWarning, setShowClearFiltersWarning] = useState(false);
const [mouseOverElement, setMouseOverElement] = useState(null as string);
const [, forceUpdate] = useReducer((x) => x + 1, 0);
const {accentColor} = useContext(QContext);
@ -125,6 +126,24 @@ const BasicAndAdvancedQueryControls = forwardRef((props: BasicAndAdvancedQueryCo
});
/*******************************************************************************
**
*******************************************************************************/
function handleMouseOverElement(name: string)
{
setMouseOverElement(name);
}
/*******************************************************************************
**
*******************************************************************************/
function handleMouseOutElement()
{
setMouseOverElement(null);
}
/*******************************************************************************
** for a given field, set its default operator for quick-filter dropdowns.
*******************************************************************************/
@ -391,11 +410,11 @@ const BasicAndAdvancedQueryControls = forwardRef((props: BasicAndAdvancedQueryCo
counter++;
return (
<React.Fragment key={i}>
<span key={i} style={{marginBottom: "0.125rem"}} onMouseOver={() => handleMouseOverElement(`queryPreview-${i}`)} onMouseOut={() => handleMouseOutElement()}>
{counter > 1 ? <span style={{marginLeft: "0.25rem", marginRight: "0.25rem"}}>{queryFilter.booleanOperator}&nbsp;</span> : <span/>}
{FilterUtils.criteriaToHumanString(tableMetaData, criteria, true)}
<XIcon position="forAdvancedQueryPreview" onClick={() => removeCriteriaByIndex(i)} />
</React.Fragment>
{mouseOverElement == `queryPreview-${i}` && <span className={`advancedQueryPreviewX-${counter - 1}`}><XIcon position="forAdvancedQueryPreview" onClick={() => removeCriteriaByIndex(i)} /></span>}
</span>
);
}
else
@ -433,7 +452,7 @@ const BasicAndAdvancedQueryControls = forwardRef((props: BasicAndAdvancedQueryCo
////////////////////////////////////////////////////////////////////////////////////////////////
if (queryFilter && queryFilter.criteria)
{
ensureAllFilterCriteriaAreActiveQuickFilters(tableMetaData, queryFilter, "modeToggleClicked");
ensureAllFilterCriteriaAreActiveQuickFilters(tableMetaData, queryFilter, "modeToggleClicked", "basic");
}
}
@ -449,7 +468,7 @@ const BasicAndAdvancedQueryControls = forwardRef((props: BasicAndAdvancedQueryCo
** make sure that all fields in the current query are on-screen as quick-filters
** (that is, if the query can be basic)
*******************************************************************************/
const ensureAllFilterCriteriaAreActiveQuickFilters = (tableMetaData: QTableMetaData, queryFilter: QQueryFilter, reason: "modeToggleClicked" | "defaultFilterLoaded" | "savedFilterSelected" | string) =>
const ensureAllFilterCriteriaAreActiveQuickFilters = (tableMetaData: QTableMetaData, queryFilter: QQueryFilter, reason: "modeToggleClicked" | "defaultFilterLoaded" | "savedFilterSelected" | string, newMode?: string) =>
{
if(!tableMetaData || !queryFilter)
{
@ -465,7 +484,8 @@ const BasicAndAdvancedQueryControls = forwardRef((props: BasicAndAdvancedQueryCo
return;
}
if(mode == "basic")
const modeToUse = newMode ?? mode;
if(modeToUse == "basic")
{
for (let i = 0; i < queryFilter?.criteria?.length; i++)
{
@ -601,6 +621,12 @@ const BasicAndAdvancedQueryControls = forwardRef((props: BasicAndAdvancedQueryCo
handleAdornmentClick={handleSetSortArrowClick}
/>);
const filterBuilderMouseEvents =
{
onMouseOver: () => handleMouseOverElement("filterBuilderButton"),
onMouseOut: () => handleMouseOutElement()
};
return (
<Box pb={mode == "advanced" ? "0.25rem" : "0"}>
@ -703,20 +729,22 @@ const BasicAndAdvancedQueryControls = forwardRef((props: BasicAndAdvancedQueryCo
<Tooltip enterDelay={500} title="Build an advanced Filter" placement="top">
<>
<Button
className="filterBuilderButton"
onClick={(e) => openFilterBuilder(e)}
{... filterBuilderMouseEvents}
startIcon={<Icon>build</Icon>}
sx={{borderRadius: "0.75rem", padding: "0.5rem", pl: "1rem", fontSize: "0.875rem", fontWeight: 500, border: `1px solid ${accentColor}`, textTransform: "none"}}
>
Filter Builder
{
countValidCriteria(queryFilter) > 0 &&
<Box sx={{backgroundColor: accentColor, marginLeft: "0.25rem", minWidth: "1rem", fontSize: "0.75rem"}} borderRadius="50%" color="#FFFFFF" position="relative" top="-2px" className="filterBuilderCountBadge">
<Box {... filterBuilderMouseEvents} sx={{backgroundColor: accentColor, marginLeft: "0.25rem", minWidth: "1rem", fontSize: "0.75rem"}} borderRadius="50%" color="#FFFFFF" position="relative" top="-2px" className="filterBuilderCountBadge">
{countValidCriteria(queryFilter) }
</Box>
}
</Button>
{
hasValidFilters && <span className="filterBuilderXIcon"><XIcon shade="accent" position="default" onClick={() => setShowClearFiltersWarning(true)} /></span>
hasValidFilters && mouseOverElement == "filterBuilderButton" && <span {... filterBuilderMouseEvents} className="filterBuilderXIcon"><XIcon shade="accent" position="default" onClick={() => setShowClearFiltersWarning(true)} /></span>
}
</>
</Tooltip>
@ -738,14 +766,15 @@ const BasicAndAdvancedQueryControls = forwardRef((props: BasicAndAdvancedQueryCo
<Box whiteSpace="nowrap" display="flex" flexShrink={1} flexGrow={1} alignItems="center">
{
<Box
className="advancedQueryString"
display="inline-block"
borderTop={`1px solid ${borderGray}`}
borderRadius="0 0 0.75rem 0.75rem"
width="100%"
sx={{fontSize: "1rem", background: "#FFFFFF"}}
minHeight={"2.5rem"}
minHeight={"2.375rem"}
p={"0.5rem"}
pb={0} // comes from the elements inside
pb={"0.125rem"}
boxShadow={"inset 0px 0px 4px 2px #EFEFED"}
>
{queryToAdvancedString()}

View File

@ -561,6 +561,14 @@ export default function FieldListMenu({idPrefix, heading, placeholder, tableMeta
const textFieldId = `field-list-dropdown-${idPrefix}-textField`;
let listItemPadding = isModeToggle ? "0.125rem": "0.5rem";
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
// for z-indexes, we set each table header to i+1, then the fields in that table to i (so they go behind it) //
// then we increment i by 2 for the next table (so the next header goes above the previous header) //
// this fixes a thing where, if one table's name wrapped to 2 lines, then when the next table below it would //
// come up, if it was only 1 line, then the second line from the previous one would bleed through. //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
let zIndex = 1;
return (
<>
<Button onClick={openMenu} {...buttonProps}>
@ -639,10 +647,12 @@ export default function FieldListMenu({idPrefix, heading, placeholder, tableMeta
marginLeft = "-1rem";
}
zIndex += 2;
return (
<React.Fragment key={tableWithFields.table?.name ?? "theTable"}>
<>
{headerContents && <ListItem sx={{position: "sticky", top: "0", backgroundColor: "#FFFFFF", zIndex: 1, padding: listItemPadding, ml: marginLeft, display: "flex", alignItems: "flex-start"}}>{headerContents}</ListItem>}
{headerContents && <ListItem sx={{position: "sticky", top: -1, zIndex: zIndex+1, padding: listItemPadding, ml: marginLeft, display: "flex", alignItems: "flex-start", backgroundImage: "linear-gradient(to bottom, rgba(255,255,255,1), rgba(255,255,255,1) 90%, rgba(255,255,255,0))"}}>{headerContents}</ListItem>}
{
tableWithFields.fields.map((field) =>
{
@ -703,7 +713,7 @@ export default function FieldListMenu({idPrefix, heading, placeholder, tableMeta
return <ListItem
key={key}
id={`field-list-dropdown-${idPrefix}-${index}`}
sx={{color: "#757575", p: 1, borderRadius: ".5rem", padding: listItemPadding, pl: paddingLeft, scrollMarginTop: "3rem", ...style}}
sx={{color: "#757575", p: 1, borderRadius: ".5rem", padding: listItemPadding, pl: paddingLeft, scrollMarginTop: "3rem", zIndex: zIndex, background: "#FFFFFF", ...style}}
onMouseOver={(event) => handleMouseOver(event, field, tableWithFields.table)}
{...onClick}
>{contents}</ListItem>;

View File

@ -53,7 +53,7 @@ export enum ValueMode
PVS_MULTI = "PVS_MULTI",
}
const getValueModeRequiredCount = (valueMode: ValueMode): number =>
export const getValueModeRequiredCount = (valueMode: ValueMode): number =>
{
switch (valueMode)
{

View File

@ -24,18 +24,16 @@ import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QField
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import {QCriteriaOperator} from "@kingsrook/qqq-frontend-core/lib/model/query/QCriteriaOperator";
import {QFilterCriteria} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterCriteria";
import {Badge, Tooltip} from "@mui/material";
import {Tooltip} from "@mui/material";
import Autocomplete from "@mui/material/Autocomplete";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import Icon from "@mui/material/Icon";
import IconButton from "@mui/material/IconButton";
import Menu from "@mui/material/Menu";
import TextField from "@mui/material/TextField";
import React, {SyntheticEvent, useContext, useState} from "react";
import QContext from "QContext";
import {QFilterCriteriaWithId} from "qqq/components/query/CustomFilterPanel";
import {getDefaultCriteriaValue, getOperatorOptions, OperatorOption, validateCriteria} from "qqq/components/query/FilterCriteriaRow";
import {getDefaultCriteriaValue, getOperatorOptions, getValueModeRequiredCount, OperatorOption, validateCriteria} from "qqq/components/query/FilterCriteriaRow";
import FilterCriteriaRowValues from "qqq/components/query/FilterCriteriaRowValues";
import XIcon from "qqq/components/query/XIcon";
import FilterUtils from "qqq/utils/qqq/FilterUtils";
@ -71,8 +69,7 @@ export const quickFilterButtonStyles = {
border: "1px solid #757575",
minWidth: "3.5rem",
minHeight: "auto",
padding: "0.375rem 0.625rem",
whiteSpace: "nowrap",
padding: "0.375rem 0.625rem", whiteSpace: "nowrap",
marginBottom: "0.5rem"
}
@ -149,6 +146,7 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
const [isOpen, setIsOpen] = useState(false);
const [anchorEl, setAnchorEl] = useState(null);
const [isMouseOver, setIsMouseOver] = useState(false);
const [criteria, setCriteria] = useState(criteriaParamIsCriteria(criteriaParam) ? criteriaParam as QFilterCriteriaWithId : null);
const [id, setId] = useState(criteriaParamIsCriteria(criteriaParam) ? (criteriaParam as QFilterCriteriaWithId).id : ++seedId);
@ -156,13 +154,29 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
const [operatorSelectedValue, setOperatorSelectedValue] = useState(getOperatorSelectedValue(operatorOptions, criteria, defaultOperator));
const [operatorInputValue, setOperatorInputValue] = useState(operatorSelectedValue?.label);
const [startIconName, setStartIconName] = useState("filter_alt");
const {criteriaIsValid, criteriaStatusTooltip} = validateCriteria(criteria, operatorSelectedValue);
const {accentColor} = useContext(QContext);
/*******************************************************************************
**
*******************************************************************************/
function handleMouseOverElement()
{
setIsMouseOver(true);
}
/*******************************************************************************
**
*******************************************************************************/
function handleMouseOutElement()
{
setIsMouseOver(false);
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// handle a change to the criteria from outside this component (e.g., the prop isn't the same as the state) //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
@ -171,7 +185,6 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
const newCriteria = criteriaParam as QFilterCriteriaWithId;
setCriteria(newCriteria);
const operatorOption = operatorOptions.filter(o => o.value == newCriteria.operator)[0];
console.log(`B: setOperatorSelectedValue [${JSON.stringify(operatorOption)}]`);
setOperatorSelectedValue(operatorOption);
setOperatorInputValue(operatorOption.label);
}
@ -202,7 +215,6 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
const operatorOption = operatorOptions.filter(o => o.value == defaultOperator)[0];
const criteria = new QFilterCriteriaWithId(fullFieldName, operatorOption?.value, getDefaultCriteriaValue());
criteria.id = id;
console.log(`C: setOperatorSelectedValue [${JSON.stringify(operatorOption)}]`);
setOperatorSelectedValue(operatorOption);
setOperatorInputValue(operatorOption?.label);
setCriteria(criteria);
@ -216,6 +228,12 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
{
setIsOpen(!isOpen);
setAnchorEl(event.currentTarget);
setTimeout(() =>
{
const element = document.getElementById("value-" + criteria.id);
element?.focus();
})
};
/*******************************************************************************
@ -236,7 +254,6 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
if (newValue)
{
console.log(`D: setOperatorSelectedValue [${JSON.stringify(newValue)}]`);
setOperatorSelectedValue(newValue);
setOperatorInputValue(newValue.label);
@ -244,10 +261,27 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
{
criteria.values = newValue.implicitValues;
}
//////////////////////////////////////////////////////////////////////////////////////////////////
// we've seen cases where switching operators can sometimes put a null in as the first value... //
// that just causes a bad time (e.g., null pointers in Autocomplete), so, get rid of that. //
//////////////////////////////////////////////////////////////////////////////////////////////////
if(criteria.values && criteria.values.length == 1 && criteria.values[0] == null)
{
criteria.values = [];
}
if(newValue.valueMode)
{
const requiredValueCount = getValueModeRequiredCount(newValue.valueMode);
if(requiredValueCount != null && criteria.values.length > requiredValueCount)
{
criteria.values.splice(requiredValueCount);
}
}
}
else
{
console.log("E: setOperatorSelectedValue [null]");
setOperatorSelectedValue(null);
setOperatorInputValue("");
}
@ -307,30 +341,9 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
e.stopPropagation();
const newCriteria = makeNewCriteria();
updateCriteria(newCriteria, false, true);
setStartIconName("filter_alt");
}
}
/*******************************************************************************
** event handler for mouse-over on the filter icon - that changes to an 'x'
** if there's a valid criteria in the quick-filter
*******************************************************************************/
const startIconMouseOver = () =>
{
if(criteriaIsValid)
{
setStartIconName("clear");
}
}
/*******************************************************************************
** event handler for mouse-out on the filter icon - always resets it.
*******************************************************************************/
const startIconMouseOut = () =>
{
setStartIconName("filter_alt");
}
/*******************************************************************************
** event handler for clicking the (x) icon that turns off this quick filter field.
** hands off control to the function that was passed in (e.g., from RecordQueryOrig).
@ -359,7 +372,6 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
const maybeNewOperatorSelectedValue = getOperatorSelectedValue(operatorOptions, criteria, defaultOperator);
if(JSON.stringify(maybeNewOperatorSelectedValue) !== JSON.stringify(operatorSelectedValue))
{
console.log(`A: setOperatorSelectedValue [${JSON.stringify(maybeNewOperatorSelectedValue)}]`);
setOperatorSelectedValue(maybeNewOperatorSelectedValue)
setOperatorInputValue(maybeNewOperatorSelectedValue?.label)
}
@ -377,11 +389,6 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
/////////////////////////
const tooComplex = criteriaParam == "tooComplex";
const tooltipEnterDelay = 500;
let startIcon = <Badge badgeContent={criteriaIsValid && !tooComplex ? 1 : 0} color="warning" variant="dot" onMouseOver={startIconMouseOver} onMouseOut={startIconMouseOut} onClick={resetCriteria}><Icon>{startIconName}</Icon></Badge>
if(criteriaIsValid)
{
startIcon = <Tooltip title={"Remove this condition from your filter"} enterDelay={tooltipEnterDelay}>{startIcon}</Tooltip>
}
let buttonAdditionalStyles: any = {};
let buttonContent = <span>{tableForField?.name != tableMetaData.name ? `${tableForField.label}: ` : ""}{fieldMetaData.label}</span>
@ -402,16 +409,19 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
valuesString = "";
}
buttonContent = (
<Tooltip title={`${operatorSelectedValue.label} ${valuesString}`} enterDelay={tooltipEnterDelay}>
{buttonContent}
</Tooltip>
);
buttonContent = (<><span style={{fontWeight: 700}}>{buttonContent}:</span>&nbsp;<span style={{fontWeight: 400}}>{operatorSelectedValue.label}&nbsp;{valuesString}</span></>);
}
const mouseEvents =
{
onMouseOver: () => handleMouseOverElement(),
onMouseOut: () => handleMouseOutElement()
};
let button = fieldMetaData && <Button
id={`quickFilter.${fullFieldName}`}
className={buttonClassName}
{...mouseEvents}
sx={{...quickFilterButtonStyles, ...buttonAdditionalStyles, mr: "0.5rem"}}
onClick={tooComplex ? noop : handleOpenMenu}
disabled={tooComplex}
@ -461,7 +471,7 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
// only show the 'x' if it's to clear out a valid criteria on the field, //
// or if we were given a callback to remove the quick-filter field from the screen //
/////////////////////////////////////////////////////////////////////////////////////
(criteriaIsValid || handleRemoveQuickFilterField) && <XIcon shade={criteriaIsValid ? "accent" : "default"} position="forQuickFilter" onClick={xClicked} />
(criteriaIsValid || handleRemoveQuickFilterField) && isMouseOver && <span {...mouseEvents}><XIcon shade={criteriaIsValid ? "accent" : "default"} position="forQuickFilter" onClick={xClicked} /></span>
}
{
isOpen && <Menu open={Boolean(anchorEl)} anchorEl={anchorEl} onClose={closeMenu} sx={{overflow: "visible"}}>

View File

@ -59,7 +59,7 @@ export default function XIcon({onClick, position, shade}: XIconProps): JSX.Eleme
else if(position == "forAdvancedQueryPreview")
{
rest = {
top: "-0.375rem",
top: "-0.5rem",
left: "-0.75rem",
}
}

View File

@ -69,6 +69,7 @@ export class LoadingState
public setLoading()
{
clearTimeout(this.slowTimeout);
this.state = "loading";
this.slowTimeout = setTimeout(() =>
{

View File

@ -23,7 +23,9 @@
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import {GridPinnedColumns} from "@mui/x-data-grid-pro";
import quickSightChart from "qqq/components/widgets/misc/QuickSightChart";
import DataGridUtils from "qqq/utils/DataGridUtils";
import TableUtils from "qqq/utils/qqq/TableUtils";
/*******************************************************************************
** member object
@ -101,6 +103,45 @@ export default class QQueryColumns
};
/*******************************************************************************
**
*******************************************************************************/
public addColumnForNewField = (table: QTableMetaData, fieldName: string, defaultVisibilityIfInMainTable: boolean): void =>
{
const [field, tableForField] = TableUtils.getFieldAndTable(table, fieldName)
let column: Column;
if(tableForField.name == table.name)
{
column = {name: field.name, isVisible: defaultVisibilityIfInMainTable, width: DataGridUtils.getColumnWidthForField(field, table)};
}
else
{
column = {name: `${tableForField.name}.${field.name}`, isVisible: false, width: DataGridUtils.getColumnWidthForField(field, null)};
}
this.columns.push(column);
}
/*******************************************************************************
**
*******************************************************************************/
public deleteColumnForOldField = (table: QTableMetaData, fieldName: string): void =>
{
for (let i = 0; i < this.columns.length; i++)
{
if(this.columns[i].name == fieldName)
{
this.columns.splice(i, 1);
return;
}
}
console.log(`Couldn't find column to be deleted, for name [${fieldName}]`);
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -44,7 +44,7 @@ import LinearProgress from "@mui/material/LinearProgress";
import MenuItem from "@mui/material/MenuItem";
import Modal from "@mui/material/Modal";
import Tooltip from "@mui/material/Tooltip";
import {ColumnHeaderFilterIconButtonProps, DataGridPro, GridCallbackDetails, GridColDef, GridColumnHeaderParams, GridColumnHeaderSortIconProps, GridColumnMenuContainer, GridColumnMenuProps, GridColumnOrderChangeParams, GridColumnPinningMenuItems, GridColumnResizeParams, GridColumnsMenuItem, GridColumnVisibilityModel, GridDensity, GridEventListener, GridFilterMenuItem, GridPinnedColumns, gridPreferencePanelStateSelector, GridPreferencePanelsValue, GridRowId, GridRowParams, GridRowsProp, GridSelectionModel, GridSortItem, GridSortModel, GridState, GridToolbarColumnsButton, GridToolbarContainer, GridToolbarDensitySelector, GridToolbarExportContainer, HideGridColMenuItem, MuiEvent, SortGridMenuItems, useGridApiContext, useGridApiEventHandler, useGridApiRef, useGridSelector} from "@mui/x-data-grid-pro";
import {ColumnHeaderFilterIconButtonProps, DataGridPro, GridCallbackDetails, GridColDef, GridColumnHeaderParams, GridColumnMenuContainer, GridColumnMenuProps, GridColumnOrderChangeParams, GridColumnPinningMenuItems, GridColumnResizeParams, GridColumnsMenuItem, GridColumnVisibilityModel, GridDensity, GridEventListener, GridFilterMenuItem, GridPinnedColumns, gridPreferencePanelStateSelector, GridPreferencePanelsValue, GridRowId, GridRowParams, GridRowsProp, GridSelectionModel, GridSortItem, GridSortModel, GridState, GridToolbarColumnsButton, GridToolbarContainer, GridToolbarDensitySelector, GridToolbarExportContainer, HideGridColMenuItem, MuiEvent, SortGridMenuItems, useGridApiContext, useGridApiEventHandler, useGridApiRef, useGridSelector} from "@mui/x-data-grid-pro";
import {GridRowModel} from "@mui/x-data-grid/models/gridRows";
import FormData from "form-data";
import React, {forwardRef, useContext, useEffect, useReducer, useRef, useState} from "react";
@ -515,6 +515,11 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
{
let label: string = tableMetaData?.label ?? "";
if(currentSavedView?.values?.get("label"))
{
label += " / " + currentSavedView?.values?.get("label");
}
if (visibleJoinTables.size > 0)
{
let joinLabels = [];
@ -675,7 +680,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
}
else
{
doSetCurrentSavedView(null);
doClearCurrentSavedView();
}
}
}
@ -1539,39 +1544,41 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
*******************************************************************************/
const doSetCurrentSavedView = (savedViewRecord: QRecord) =>
{
if(savedViewRecord == null)
{
console.log("doSetCurrentView called with a null view record - calling doClearCurrentSavedView instead.");
doClearCurrentSavedView();
return;
}
setCurrentSavedView(savedViewRecord);
if(savedViewRecord)
{
(async () =>
{
const viewJson = savedViewRecord.values.get("viewJson")
const newView = RecordQueryView.buildFromJSON(viewJson);
const viewJson = savedViewRecord.values.get("viewJson")
const newView = RecordQueryView.buildFromJSON(viewJson);
activateView(newView);
activateView(newView);
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// todo - we used to be able to set "warnings" here (i think, like, for if a field got deleted from a table... //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// setWarningAlert(models.warning);
////////////////////////////////////////////////////////////////
// todo can/should/does this move into the view's "identity"? //
////////////////////////////////////////////////////////////////
localStorage.setItem(currentSavedViewLocalStorageKey, `${savedViewRecord.values.get("id")}`);
}
////////////////////////////////////////////////////////////////
// todo can/should/does this move into the view's "identity"? //
////////////////////////////////////////////////////////////////
localStorage.setItem(currentSavedViewLocalStorageKey, `${savedViewRecord.values.get("id")}`);
})()
}
else
{
localStorage.removeItem(currentSavedViewLocalStorageKey);
}
/*******************************************************************************
** wrapper around un-setting current saved view and removing its id from local-stroage
*******************************************************************************/
const doClearCurrentSavedView = () =>
{
setCurrentSavedView(null);
localStorage.removeItem(currentSavedViewLocalStorageKey);
}
/*******************************************************************************
**
*******************************************************************************/
const buildTableDefaultView = (): RecordQueryView =>
const buildTableDefaultView = (tableMetaData: QTableMetaData): RecordQueryView =>
{
const newDefaultView = new RecordQueryView();
newDefaultView.queryFilter = new QQueryFilter([], [new QFilterOrderBy(tableMetaData.primaryKeyField, false)]);
@ -1616,7 +1623,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
///////////////////////////////////////////////
// activate a new default view for the table //
///////////////////////////////////////////////
activateView(buildTableDefaultView())
activateView(buildTableDefaultView(tableMetaData))
}
}
@ -1634,6 +1641,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
{
const jobError = processResult as QJobError;
console.error("Could not retrieve saved filter: " + jobError.userFacingError);
setAlertContent("There was an error loading the selected view.");
}
else
{
@ -1646,29 +1654,138 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
// what they're like in the backend); similarly, set anything that's unset. //
//////////////////////////////////////////////////////////////////////////////
const viewJson = qRecord.values.get("viewJson")
const view = RecordQueryView.buildFromJSON(viewJson);
view.viewIdentity = "savedView:" + id;
const newView = RecordQueryView.buildFromJSON(viewJson);
setWarningAlert(null);
reconcileCurrentTableMetaDataWithView(newView, "loadingSavedView");
newView.viewIdentity = "savedView:" + id;
///////////////////////////////////////////////////////////////////
// e.g., translate possible values from ids to objects w/ labels //
///////////////////////////////////////////////////////////////////
await FilterUtils.cleanupValuesInFilerFromQueryString(qController, tableMetaData, view.queryFilter);
await FilterUtils.cleanupValuesInFilerFromQueryString(qController, tableMetaData, newView.queryFilter);
///////////////////////////
// set columns if absent //
///////////////////////////
if(!view.queryColumns || !view.queryColumns.columns || view.queryColumns.columns?.length == 0)
if(!newView.queryColumns || !newView.queryColumns.columns || newView.queryColumns.columns?.length == 0)
{
view.queryColumns = QQueryColumns.buildDefaultForTable(tableMetaData);
newView.queryColumns = QQueryColumns.buildDefaultForTable(tableMetaData);
}
qRecord.values.set("viewJson", JSON.stringify(view))
qRecord.values.set("viewJson", JSON.stringify(newView))
}
return (qRecord);
}
/*******************************************************************************
** after a page-load, or before activating a saved view, make sure that no
** fields are missing from its column list, and that no deleted-fields are still
** being used.
*******************************************************************************/
const reconcileCurrentTableMetaDataWithView = (view: RecordQueryView, useCase: "initialPageLoad" | "loadingSavedView") =>
{
let changedView = false;
const removedFieldNames = new Set<string>();
if (view.queryColumns?.columns?.length > 0)
{
const fieldNamesInView: { [name: string]: boolean } = {};
view.queryColumns?.columns?.forEach(column => fieldNamesInView[column.name] = true);
for (let i = 0; i < tableDefaultView?.queryColumns?.columns.length; i++)
{
const currentColumn = tableDefaultView?.queryColumns?.columns[i];
if (!fieldNamesInView[currentColumn.name])
{
console.log(`Adding a new column to this view ${currentColumn.name}`);
view.queryColumns.addColumnForNewField(tableMetaData, currentColumn.name, useCase == "initialPageLoad");
changedView = true;
}
else
{
delete fieldNamesInView[currentColumn.name];
}
}
//////////////////////////////////////////////////////////////
// delete, from the view, any fields no longer in the table //
//////////////////////////////////////////////////////////////
for (let fieldName in fieldNamesInView)
{
console.log(`Deleting an old column from this view ${fieldName}`);
view.queryColumns.deleteColumnForOldField(tableMetaData, fieldName);
changedView = true;
removedFieldNames.add(fieldName);
}
}
/////////////////////////////////////////
// look for deleted fields as criteria //
/////////////////////////////////////////
for (let i = 0; i < view?.queryFilter?.criteria?.length; i++)
{
const fieldName = view.queryFilter.criteria[i].fieldName;
const [field, fieldTable] = TableUtils.getFieldAndTable(tableMetaData, fieldName);
if (field == null)
{
console.log(`Deleting an old criteria field from this view ${fieldName}`);
view.queryFilter.criteria.splice(i, 1);
changedView = true;
removedFieldNames.add(fieldName);
i--;
}
}
/////////////////////////////////////////
// look for deleted fields as orderBys //
/////////////////////////////////////////
for (let i = 0; i < view?.queryFilter?.orderBys?.length; i++)
{
const fieldName = view.queryFilter.orderBys[i].fieldName;
const [field, fieldTable] = TableUtils.getFieldAndTable(tableMetaData, fieldName);
if (field == null)
{
console.log(`Deleting an old orderBy field from this view ${fieldName}`);
view.queryFilter.orderBys.splice(i, 1);
changedView = true;
removedFieldNames.add(fieldName);
i--;
}
}
//////////////////////////////////////////////
// look for deleted fields as quick-filters //
//////////////////////////////////////////////
for (let i = 0; i < view?.quickFilterFieldNames?.length; i++)
{
const fieldName = view.quickFilterFieldNames[i];
const [field, fieldTable] = TableUtils.getFieldAndTable(tableMetaData, fieldName);
if (field == null)
{
console.log(`Deleting an old quikc-filter field from this view ${fieldName}`);
view.quickFilterFieldNames.splice(i, 1);
changedView = true;
removedFieldNames.add(fieldName);
i--;
}
}
if (changedView && useCase == "initialPageLoad")
{
activateView(view);
}
const removedFieldCount = removedFieldNames.size;
if(removedFieldCount > 0)
{
const plural = removedFieldCount > 1;
setWarningAlert(`${removedFieldCount} field${plural ? "s" : ""} that ${plural ? "were" : "was"} part of this view ${plural ? "are" : "is"} no longer in this table, and ${plural ? "were" : "was"} removed from this view (${[...removedFieldNames.values()].join(", ")}).`);
}
}
/*******************************************************************************
** event handler for selecting 'filter' action from columns menu in advanced mode.
*******************************************************************************/
@ -1731,12 +1848,13 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
{
await navigator.clipboard.writeText(data);
setSuccessAlert(`Copied ${counter} ${qFieldMetaData.label} value${counter == 1 ? "" : "s"}.`);
setTimeout(() => setSuccessAlert(null), 3000);
}
else
{
setSuccessAlert(`There are no ${qFieldMetaData.label} values to copy.`);
setWarningAlert(`There are no ${qFieldMetaData.label} values to copy.`);
setTimeout(() => setWarningAlert(null), 3000);
}
setTimeout(() => setSuccessAlert(null), 3000);
}
};
@ -1811,7 +1929,6 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
</MenuItem>
<HideGridColMenuItem onClick={hideMenu} column={currentColumn!} />
<GridColumnsMenuItem onClick={hideMenu} column={currentColumn!} />
<Divider />
<GridColumnPinningMenuItems onClick={hideMenu} column={currentColumn!} />
@ -2157,6 +2274,13 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
setTableProcesses(ProcessUtils.getProcessesForTable(metaData, tableName)); // these are the ones to show in the dropdown
setAllTableProcesses(ProcessUtils.getProcessesForTable(metaData, tableName, true)); // these include hidden ones (e.g., to find the bulks)
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// now that we know the table - build a default view - initially, only used by SavedViews component, for showing if there's anything to be saved. //
// but also used when user selects new-view from the view menu //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
const newDefaultView = buildTableDefaultView(tableMetaData);
setTableDefaultView(newDefaultView);
setPageState("loadedMetaData");
})();
}
@ -2171,13 +2295,6 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
(async () =>
{
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// now that we know the table - build a default view - initially, only used by SavedViews component, for showing if there's anything to be saved. //
// but also used when user selects new-view from the view menu //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
const newDefaultView = buildTableDefaultView();
setTableDefaultView(newDefaultView);
//////////////////////////////////////////////////////////////////////////////////////////////
// once we've loaded meta data, let's check the location to see if we should open a process //
//////////////////////////////////////////////////////////////////////////////////////////////
@ -2255,7 +2372,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
/////////////////////////////////////////////////////////////////////////////////////////////
// make sure that we clear out any currently saved view - we're no longer in such a state. //
/////////////////////////////////////////////////////////////////////////////////////////////
doSetCurrentSavedView(null);
doClearCurrentSavedView();
}
catch(e)
{
@ -2331,68 +2448,32 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
console.log("page state is loadedView - going to preparingGrid...");
setPageState("preparingGrid");
(async () =>
//////////////////////////////////////////////////////////////////////////////////////////////////////
// check if any new columns have been added to the table since last time this view was activated... //
// or if anything in the view is no longer in the table //
//////////////////////////////////////////////////////////////////////////////////////////////////////
reconcileCurrentTableMetaDataWithView(view, "initialPageLoad");
////////////////////////////////////////////////////////////////////////////////////////
// this ref may not be defined on the initial render, so, make this call in a timeout //
////////////////////////////////////////////////////////////////////////////////////////
setTimeout(() =>
{
const visibleJoinTables = getVisibleJoinTables();
// @ts-ignore
basicAndAdvancedQueryControlsRef?.current?.ensureAllFilterCriteriaAreActiveQuickFilters(view.queryFilter, "defaultFilterLoaded")
});
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// todo - we used to be able to set "warnings" here (i think, like, for if a field got deleted from a table... //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// setWarningAlert(models.warning);
console.log("finished preparing grid, going to page state ready");
setPageState("ready");
////////////////////////////////////////////////////////////////////////////////////////
// this ref may not be defined on the initial render, so, make this call in a timeout //
////////////////////////////////////////////////////////////////////////////////////////
setTimeout(() =>
{
// @ts-ignore
basicAndAdvancedQueryControlsRef?.current?.ensureAllFilterCriteriaAreActiveQuickFilters(view.queryFilter, "defaultFilterLoaded")
});
////////////////////////////////////////////
// if we need a variant, show that prompt //
////////////////////////////////////////////
if (tableMetaData?.usesVariants && !tableVariant)
{
promptForTableVariantSelection();
}
//////////////////////////////////////////////////////////////////////////////////////////////////
// make sure that any if any sort columns are from a join table, that the join table is visible //
// todo - figure out what this is, see if still needed, etc...
//////////////////////////////////////////////////////////////////////////////////////////////////
/*
let resetColumnSortModel = false;
for (let i = 0; i < columnSortModel.length; i++)
{
const gridSortItem = columnSortModel[i];
if (gridSortItem.field.indexOf(".") > -1)
{
const tableName = gridSortItem.field.split(".")[0];
if (!visibleJoinTables?.has(tableName))
{
columnSortModel.splice(i, 1);
setColumnSortModel(columnSortModel);
// todo - need to setQueryFilter?
resetColumnSortModel = true;
i--;
}
}
}
if (resetColumnSortModel && latestQueryId > 0)
{
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
// let the next render (since columnSortModel is watched below) build the filter, using the new columnSort //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
return;
}
*/
console.log("finished preparing grid, going to page state ready");
setPageState("ready");
////////////////////////////////////////////
// if we need a variant, show that prompt //
////////////////////////////////////////////
if (tableMetaData?.usesVariants && !tableVariant)
{
promptForTableVariantSelection();
return;
}
})();
return (getLoadingScreen());
}
@ -2584,13 +2665,13 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
<BaseLayout>
<Box display="flex" justifyContent="space-between">
<Box>
<Typography textTransform="capitalize" variant="h3" noWrap>
<Typography textTransform="capitalize" variant="h3">
{pageLoadingState.isLoading() && ""}
{pageLoadingState.isLoadingSlow() && "Loading..."}
{pageLoadingState.isNotLoading() && getPageHeader(tableMetaData, visibleJoinTables, tableVariant)}
</Typography>
</Box>
<Box>
<Box whiteSpace="nowrap">
<GotoRecordButton metaData={metaData} tableMetaData={tableMetaData} />
<Box display="inline-block" width="150px">
{
@ -2622,37 +2703,31 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
</iframe>
*/}
<Box mb={3}>
{alertContent ? (
<Box mb={3}>
<Alert
severity="error"
onClose={() =>
{
setAlertContent(null);
}}
>
{alertContent}
</Alert>
</Box>
) : (
""
)}
{
alertContent ? (
<Collapse in={Boolean(alertContent)}>
<Alert severity="error" sx={{mt: 1.5, mb: 0.5}} onClose={() => setAlertContent(null)}>{alertContent}</Alert>
</Collapse>
) : null
}
{
(tableLabel && showSuccessfullyDeletedAlert) ? (
<Alert color="success" sx={{mb: 3}} onClose={() => setShowSuccessfullyDeletedAlert(false)}>{`${tableLabel} successfully deleted`}</Alert>
<Collapse in={Boolean(showSuccessfullyDeletedAlert)}>
<Alert color="success" sx={{mt: 1.5, mb: 0.5}} onClose={() => setShowSuccessfullyDeletedAlert(false)}>{`${tableLabel} successfully deleted`}</Alert>
</Collapse>
) : null
}
{
(successAlert) ? (
<Collapse in={Boolean(successAlert)}>
<Alert color="success" sx={{mb: 3}} onClose={() => setSuccessAlert(null)}>{successAlert}</Alert>
<Alert color="success" sx={{mt: 1.5, mb: 0.5}} onClose={() => setSuccessAlert(null)}>{successAlert}</Alert>
</Collapse>
) : null
}
{
(warningAlert) ? (
<Collapse in={Boolean(warningAlert)}>
<Alert color="warning" icon={<Icon>warning</Icon>} sx={{mb: 3}} onClose={() => setWarningAlert(null)}>{warningAlert}</Alert>
<Alert color="warning" icon={<Icon>warning</Icon>} sx={{mt: 1.5, mb: 0.5}} onClose={() => setWarningAlert(null)}>{warningAlert}</Alert>
</Collapse>
) : null
}
@ -2700,11 +2775,8 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
}
}}
localeText={{
toolbarFilters: "Filter", // label on the filters button. we prefer singular (1 filter has many "conditions" in it).
toolbarFiltersLabel: "", // setting these 3 to "" turns off the "Show Filters" and "Hide Filters" tooltip (which can get in the way of the actual filters panel)
toolbarFiltersTooltipShow: "",
toolbarFiltersTooltipHide: "",
toolbarFiltersTooltipActive: count => count !== 1 ? `${count} conditions` : `${count} condition`
columnMenuSortAsc: "Sort ascending",
columnMenuSortDesc: "Sort descending",
}}
pinnedColumns={pinnedColumns}
onPinnedColumnsChange={handlePinnedColumnsChange}

View File

@ -63,6 +63,11 @@ public class QSeleniumLib
private boolean autoHighlight = false;
//////////////////////////////////////////////////////////////////////////////////////
// useful to use on a WebElement, in a call like: .findElement(QSeleniumLib.PARENT) //
//////////////////////////////////////////////////////////////////////////////////////
public static final By PARENT = By.xpath("./..");
/*******************************************************************************
@ -245,6 +250,18 @@ public class QSeleniumLib
/*******************************************************************************
**
*******************************************************************************/
public void moveMouseCursorToElement(WebElement element)
{
Actions actions = new Actions(driver);
actions.moveToElement(element);
actions.perform();
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -57,6 +57,29 @@ public class QueryScreenLib
/*******************************************************************************
**
*******************************************************************************/
public void clickAdvancedFilterClearIcon()
{
qSeleniumLib.moveMouseCursorToElement(qSeleniumLib.waitForSelector(".filterBuilderButton"));
qSeleniumLib.waitForSelector(".filterBuilderXIcon BUTTON").click();
qSeleniumLib.waitForSelectorContaining("BUTTON", "Yes").click();
}
/*******************************************************************************
**
*******************************************************************************/
public void clickQuickFilterClearIcon(String fieldName)
{
qSeleniumLib.moveMouseCursorToElement(qSeleniumLib.waitForSelector("#quickFilter\\." + fieldName));
qSeleniumLib.waitForSelector("#quickFilter\\." + fieldName + "+span button").click();
}
/*******************************************************************************
**
*******************************************************************************/
@ -97,6 +120,16 @@ public class QueryScreenLib
/*******************************************************************************
**
*******************************************************************************/
public void assertQuickFilterButtonDoesNotIndicateActiveFilter(String fieldName)
{
qSeleniumLib.waitForSelectorToNotExist("#quickFilter\\." + fieldName + ".filterActive");
}
/*******************************************************************************
**
*******************************************************************************/
@ -132,7 +165,26 @@ public class QueryScreenLib
/*******************************************************************************
**
*******************************************************************************/
public void addQueryFilterInput(QSeleniumLib qSeleniumLib, int index, String fieldLabel, String operator, String value, String booleanOperator)
public void assertSavedViewNameOnScreen(String savedViewName)
{
qSeleniumLib.waitForSelectorContaining("H3", savedViewName);
}
/*******************************************************************************
**
*******************************************************************************/
public WebElement waitForDataGridCellContaining(String containingText)
{
return qSeleniumLib.waitForSelectorContaining("DIV.MuiDataGrid-cell", containingText);
}
/*******************************************************************************
**
*******************************************************************************/
public void addAdvancedQueryFilterInput(QSeleniumLib qSeleniumLib, int index, String fieldLabel, String operator, String value, String booleanOperator)
{
if(index > 0)
{

View File

@ -26,6 +26,7 @@ import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import com.kingsrook.qqq.frontend.materialdashboard.selenium.lib.QBaseSeleniumTest;
import com.kingsrook.qqq.frontend.materialdashboard.selenium.lib.QSeleniumLib;
import com.kingsrook.qqq.frontend.materialdashboard.selenium.lib.javalin.QSeleniumJavalin;
import org.apache.commons.io.FileUtils;
import org.junit.jupiter.api.Test;
@ -89,7 +90,7 @@ public class DashboardTableWidgetExportTest extends QBaseSeleniumTest
// click the export button //
/////////////////////////////
qSeleniumLib.waitForSelector("#SampleTableWidget h6")
.findElement(By.xpath("./.."))
.findElement(QSeleniumLib.PARENT)
.findElement(By.cssSelector("button"))
.click();

View File

@ -19,7 +19,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.frontend.materialdashboard.selenium.tests;
package com.kingsrook.qqq.frontend.materialdashboard.selenium.tests.query;
import java.net.URLEncoder;
@ -98,6 +98,16 @@ public class QueryScreenFilterInUrlAdvancedModeTest extends QBaseSeleniumTest
qSeleniumLib.waitForSelector("input[value=\"1701\"]");
qSeleniumLib.waitForSelector("input[value=\"74656\"]");
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// click the x to remove a condition from the filter (in the on-screen preview) //
// reload the page first, so filter-panel won't be up (clicking backdrop doesn't seem to be closing it like it should...) //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/peopleApp/greetingsApp/person?filter=" + URLEncoder.encode(filterJSON, StandardCharsets.UTF_8), "Person");
qSeleniumLib.highlightElement(qSeleniumLib.waitForSelectorContaining(".advancedQueryString DIV DIV", "1701"));
qSeleniumLib.moveMouseCursorToElement(qSeleniumLib.waitForSelectorContaining(".advancedQueryString DIV DIV", "1701"));
qSeleniumLib.waitForSelector(".advancedQueryPreviewX-0 button").click();
queryScreenLib.assertNoFilterButtonBadge(1);
//////////////////////////////////////
// an IN for a possible-value field //
//////////////////////////////////////
@ -162,8 +172,7 @@ public class QueryScreenFilterInUrlAdvancedModeTest extends QBaseSeleniumTest
////////////////
// remove one //
////////////////
qSeleniumLib.tryMultiple(3, () -> qSeleniumLib.waitForSelector(".filterBuilderXIcon BUTTON").click());
qSeleniumLib.waitForSelectorContaining("BUTTON", "Yes").click();
queryScreenLib.clickAdvancedFilterClearIcon();
queryScreenLib.assertNoFilterButtonBadge(1);
// qSeleniumLib.waitForever();

View File

@ -19,7 +19,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.frontend.materialdashboard.selenium.tests;
package com.kingsrook.qqq.frontend.materialdashboard.selenium.tests.query;
import java.net.URLEncoder;
@ -147,11 +147,15 @@ public class QueryScreenFilterInUrlBasicModeTest extends QBaseSeleniumTest
qSeleniumLib.waitForSelector("input[value=\"starts with\"]");
qSeleniumLib.waitForSelector("input[value=\"Dar\"]");
////////////////
// remove one //
////////////////
// todo! qSeleniumLib.waitForSelectorContaining(".MuiIcon-root", "close").click();
// todo! assertQuickFilterButtonBadge(1);
////////////////////////////////
// remove one, then the other //
////////////////////////////////
qSeleniumLib.clickBackdrop();
queryScreenLib.clickQuickFilterClearIcon("createDate");
queryScreenLib.assertQuickFilterButtonIndicatesActiveFilter("firstName");
queryScreenLib.assertQuickFilterButtonDoesNotIndicateActiveFilter("createDate");
queryScreenLib.clickQuickFilterClearIcon("firstName");
queryScreenLib.assertQuickFilterButtonDoesNotIndicateActiveFilter("firstName");
// qSeleniumLib.waitForever();
}

View File

@ -19,7 +19,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.frontend.materialdashboard.selenium.tests;
package com.kingsrook.qqq.frontend.materialdashboard.selenium.tests.query;
import com.kingsrook.qqq.frontend.materialdashboard.selenium.lib.QBaseSeleniumTest;
@ -70,7 +70,7 @@ public class QueryScreenTest extends QBaseSeleniumTest
// open the filter window, enter a value, wait for query to re-run //
/////////////////////////////////////////////////////////////////////
qSeleniumJavalin.beginCapture();
queryScreenLib.addQueryFilterInput(qSeleniumLib, 0, "Id", "equals", "1", null);
queryScreenLib.addAdvancedQueryFilterInput(qSeleniumLib, 0, "Id", "equals", "1", null);
///////////////////////////////////////////////////////////////////
// assert that query & count both have the expected filter value //
@ -86,14 +86,13 @@ public class QueryScreenTest extends QBaseSeleniumTest
///////////////////////////////////////
qSeleniumLib.waitForSeconds(1); // todo grr.
qSeleniumLib.waitForSelector(QQQMaterialDashboardSelectors.BREADCRUMB_HEADER).click();
qSeleniumLib.waitForSelectorContaining(".filterBuilderCountBadge", "1");
queryScreenLib.assertFilterButtonBadge(1);
///////////////////////////////////////////////////////////////////
// click the 'x' clear icon, then yes, then expect another query //
///////////////////////////////////////////////////////////////////
qSeleniumJavalin.beginCapture();
qSeleniumLib.tryMultiple(3, () -> qSeleniumLib.waitForSelector(".filterBuilderXIcon BUTTON").click());
qSeleniumLib.waitForSelectorContaining("BUTTON", "Yes").click();
queryScreenLib.clickAdvancedFilterClearIcon();
////////////////////////////////////////////////////////////////////
// assert that query & count both no longer have the filter value //
@ -121,8 +120,8 @@ public class QueryScreenTest extends QBaseSeleniumTest
queryScreenLib.clickFilterButton();
qSeleniumJavalin.beginCapture();
queryScreenLib.addQueryFilterInput(qSeleniumLib, 0, "First Name", "contains", "Dar", "Or");
queryScreenLib.addQueryFilterInput(qSeleniumLib, 1, "First Name", "contains", "Jam", "Or");
queryScreenLib.addAdvancedQueryFilterInput(qSeleniumLib, 0, "First Name", "contains", "Dar", "Or");
queryScreenLib.addAdvancedQueryFilterInput(qSeleniumLib, 1, "First Name", "contains", "Jam", "Or");
String expectedFilterContents0 = """
{"fieldName":"firstName","operator":"CONTAINS","values":["Dar"]}""";

View File

@ -19,7 +19,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.frontend.materialdashboard.selenium.tests;
package com.kingsrook.qqq.frontend.materialdashboard.selenium.tests.query;
import java.net.URLEncoder;
@ -90,13 +90,13 @@ public class SavedViewsTest extends QBaseSeleniumTest
/////////////////////////////////////////////////////
qSeleniumLib.waitForSelectorContaining("LI", "Some People").click();
qSeleniumLib.waitForCondition("Current URL should have view id", () -> driver.getCurrentUrl().endsWith("/person/savedView/2"));
qSeleniumLib.waitForSelectorContaining("BUTTON", "Some People");
queryScreenLib.assertSavedViewNameOnScreen("Some People");
//////////////////////////////
// click into a view screen //
//////////////////////////////
qSeleniumLib.waitForSeconds(1); // wait for the filters menu to fully disappear? if this doesn't work, try a different word to look for...
qSeleniumLib.waitForSelectorContaining("DIV.MuiDataGrid-cell", "jdoe@kingsrook.com").click();
queryScreenLib.waitForDataGridCellContaining("jdoe@kingsrook.com").click();
qSeleniumLib.waitForSelectorContaining("H5", "Viewing Person: John Doe");
///////////////////////////////////////////////////
@ -105,7 +105,7 @@ public class SavedViewsTest extends QBaseSeleniumTest
///////////////////////////////////////////////////
qSeleniumLib.waitForSelectorContaining("A", "Person").click();
qSeleniumLib.waitForCondition("Current URL should have View id", () -> driver.getCurrentUrl().endsWith("/person/savedView/2"));
qSeleniumLib.waitForSelectorContaining("BUTTON", "Some People");
queryScreenLib.assertSavedViewNameOnScreen("Some People");
queryScreenLib.assertQuickFilterButtonIndicatesActiveFilter("firstName");
//////////////////////
@ -123,7 +123,7 @@ public class SavedViewsTest extends QBaseSeleniumTest
//////////////////////////////
// click into a view screen //
//////////////////////////////
qSeleniumLib.waitForSelectorContaining("DIV.MuiDataGrid-cell", "jdoe@kingsrook.com").click();
queryScreenLib.waitForDataGridCellContaining("jdoe@kingsrook.com").click();
qSeleniumLib.waitForSelectorContaining("H5", "Viewing Person: John Doe");
///////////////////////////////////////////////////////////////////////////////
@ -133,7 +133,7 @@ public class SavedViewsTest extends QBaseSeleniumTest
qSeleniumJavalin.beginCapture();
qSeleniumLib.waitForSelectorContaining("A", "Person").click();
qSeleniumLib.waitForCondition("Current URL should have filter id", () -> driver.getCurrentUrl().endsWith("/person/savedView/2"));
qSeleniumLib.waitForSelectorContaining("BUTTON", "Some People");
queryScreenLib.assertSavedViewNameOnScreen("Some People");
qSeleniumLib.waitForSelectorContaining("DIV", "Unsaved Changes");
CapturedContext capturedContext = qSeleniumJavalin.waitForCapturedPath("/data/person/query");
assertTrue(capturedContext.getBody().contains("Kelkhoff"));
@ -143,16 +143,7 @@ public class SavedViewsTest extends QBaseSeleniumTest
// navigate to the table with a View in the URL //
//////////////////////////////////////////////////
String filter = """
{
"criteria":
[
{
"fieldName": "id",
"operator": "LESS_THAN",
"values": [10]
}
]
}
{"criteria":[{"fieldName":"id", "operator":"LESS_THAN", "values":[10]}]}
""".replace('\n', ' ').replaceAll(" ", "");
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/peopleApp/greetingsApp/person?filter=" + URLEncoder.encode(filter, StandardCharsets.UTF_8), "Person");
qSeleniumLib.waitForSelectorContaining("BUTTON", "Save View As");
@ -160,7 +151,7 @@ public class SavedViewsTest extends QBaseSeleniumTest
//////////////////////////////
// click into a view screen //
//////////////////////////////
qSeleniumLib.waitForSelectorContaining("DIV.MuiDataGrid-cell", "jdoe@kingsrook.com").click();
queryScreenLib.waitForDataGridCellContaining("jdoe@kingsrook.com").click();
qSeleniumLib.waitForSelectorContaining("H5", "Viewing Person: John Doe");
/////////////////////////////////////////////////////////////////////////////////