mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-17 12:50:43 +00:00
CE-798 Post-demo style updates; add concept of reconciling current table definition w/ view (e.g., add/delete columns from tables); test updates
This commit is contained in:
@ -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>
|
||||
|
@ -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)
|
||||
|
@ -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} </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()}
|
||||
|
@ -53,7 +53,7 @@ export enum ValueMode
|
||||
PVS_MULTI = "PVS_MULTI",
|
||||
}
|
||||
|
||||
const getValueModeRequiredCount = (valueMode: ValueMode): number =>
|
||||
export const getValueModeRequiredCount = (valueMode: ValueMode): number =>
|
||||
{
|
||||
switch (valueMode)
|
||||
{
|
||||
|
@ -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> <span style={{fontWeight: 400}}>{operatorSelectedValue.label} {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"}}>
|
||||
|
@ -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",
|
||||
}
|
||||
}
|
||||
|
@ -69,6 +69,7 @@ export class LoadingState
|
||||
|
||||
public setLoading()
|
||||
{
|
||||
clearTimeout(this.slowTimeout);
|
||||
this.state = "loading";
|
||||
this.slowTimeout = setTimeout(() =>
|
||||
{
|
||||
|
@ -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}]`);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -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}
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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();
|
@ -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();
|
||||
}
|
@ -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"]}""";
|
@ -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");
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
Reference in New Issue
Block a user