mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-21 22:58:43 +00:00
Compare commits
17 Commits
snapshot-f
...
snapshot-f
Author | SHA1 | Date | |
---|---|---|---|
809f01e43e | |||
5f3957e897 | |||
e6b9b34206 | |||
dee7bc693e | |||
948aee70fd | |||
f0c1af18d0 | |||
fa65d6c0ad | |||
630f0d2dc1 | |||
d795bd8427 | |||
d6c9bf79b1 | |||
677b93a09f | |||
314bf0fd67 | |||
76642f13e9 | |||
0eaf171523 | |||
b137b3346d | |||
63479ba282 | |||
967c557a58 |
@ -6,7 +6,7 @@
|
||||
"@auth0/auth0-react": "1.10.2",
|
||||
"@emotion/react": "11.7.1",
|
||||
"@emotion/styled": "11.6.0",
|
||||
"@kingsrook/qqq-frontend-core": "1.0.86",
|
||||
"@kingsrook/qqq-frontend-core": "1.0.87",
|
||||
"@mui/icons-material": "5.4.1",
|
||||
"@mui/material": "5.11.1",
|
||||
"@mui/styles": "5.11.1",
|
||||
|
@ -354,8 +354,7 @@ const CommandMenu = ({metaData}: Props) =>
|
||||
<Grid container columnSpacing={5} rowSpacing={1}>
|
||||
<Grid item xs={6} className={classes.item}><span className={classes.keyboardKey}>n</span>Create a New Record</Grid>
|
||||
<Grid item xs={6} className={classes.item}><span className={classes.keyboardKey}>r</span>Refresh the Query</Grid>
|
||||
<Grid item xs={6} className={classes.item}><span className={classes.keyboardKey}>c</span>Open the Columns Panel</Grid>
|
||||
<Grid item xs={6} className={classes.item}><span className={classes.keyboardKey}>f</span>Open the Filter Panel</Grid>
|
||||
<Grid item xs={6} className={classes.item}><span className={classes.keyboardKey}>f</span>Open the Filter Builder (Advanced mode only)</Grid>
|
||||
</Grid>
|
||||
|
||||
<Typography variant="h6" pt={3}>Record View</Typography>
|
||||
|
@ -28,7 +28,7 @@ import QContext from "QContext";
|
||||
|
||||
interface Props
|
||||
{
|
||||
helpContents: QHelpContent[];
|
||||
helpContents: null | QHelpContent | QHelpContent[];
|
||||
roles: string[];
|
||||
heading?: string;
|
||||
helpContentKey?: string;
|
||||
@ -93,9 +93,27 @@ const getMatchingHelpContent = (helpContents: QHelpContent[], roles: string[]):
|
||||
/*******************************************************************************
|
||||
** test if a list of help contents would find any matches from a list of roles.
|
||||
*******************************************************************************/
|
||||
export const hasHelpContent = (helpContents: QHelpContent[], roles: string[]) =>
|
||||
export const hasHelpContent = (helpContents: null | QHelpContent | QHelpContent[], roles: string[]) =>
|
||||
{
|
||||
return getMatchingHelpContent(helpContents, roles) != null;
|
||||
return getMatchingHelpContent(nullOrSingletonOrArrayToArray(helpContents), roles) != null;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
const nullOrSingletonOrArrayToArray = (helpContents: null | QHelpContent | QHelpContent[]): QHelpContent[] =>
|
||||
{
|
||||
let array: QHelpContent[] = [];
|
||||
if(Array.isArray(helpContents))
|
||||
{
|
||||
array = helpContents;
|
||||
}
|
||||
else if(helpContents != null)
|
||||
{
|
||||
array.push(helpContents);
|
||||
}
|
||||
return (array);
|
||||
}
|
||||
|
||||
|
||||
@ -106,7 +124,8 @@ export const hasHelpContent = (helpContents: QHelpContent[], roles: string[]) =>
|
||||
function HelpContent({helpContents, roles, heading, helpContentKey}: Props): JSX.Element
|
||||
{
|
||||
const {helpHelpActive} = useContext(QContext);
|
||||
let selectedHelpContent = getMatchingHelpContent(helpContents, roles);
|
||||
const helpContentsArray = nullOrSingletonOrArrayToArray(helpContents);
|
||||
let selectedHelpContent = getMatchingHelpContent(helpContentsArray, roles);
|
||||
|
||||
let content = null;
|
||||
if (helpHelpActive)
|
||||
|
@ -30,7 +30,7 @@ import Box from "@mui/material/Box";
|
||||
import Button from "@mui/material/Button";
|
||||
import Menu from "@mui/material/Menu";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import React, {SyntheticEvent, useContext, useState} from "react";
|
||||
import React, {SyntheticEvent, useContext, useReducer, useState} from "react";
|
||||
import QContext from "QContext";
|
||||
import {QFilterCriteriaWithId} from "qqq/components/query/CustomFilterPanel";
|
||||
import {getDefaultCriteriaValue, getOperatorOptions, getValueModeRequiredCount, OperatorOption, validateCriteria} from "qqq/components/query/FilterCriteriaRow";
|
||||
@ -148,7 +148,10 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
const [isMouseOver, setIsMouseOver] = useState(false);
|
||||
|
||||
const [criteria, setCriteria] = useState(criteriaParamIsCriteria(criteriaParam) ? criteriaParam as QFilterCriteriaWithId : null);
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// copy the criteriaParam to a new object in here - so changes won't apply until user closes the menu //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
const [criteria, setCriteria] = useState(criteriaParamIsCriteria(criteriaParam) ? Object.assign({}, criteriaParam) as QFilterCriteriaWithId : null);
|
||||
const [id, setId] = useState(criteriaParamIsCriteria(criteriaParam) ? (criteriaParam as QFilterCriteriaWithId).id : ++seedId);
|
||||
|
||||
const [operatorSelectedValue, setOperatorSelectedValue] = useState(getOperatorSelectedValue(operatorOptions, criteria, defaultOperator));
|
||||
@ -158,6 +161,11 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
|
||||
|
||||
const {accentColor} = useContext(QContext);
|
||||
|
||||
//////////////////////
|
||||
// ole' faithful... //
|
||||
//////////////////////
|
||||
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
@ -182,15 +190,30 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if (criteriaParamIsCriteria(criteriaParam) && JSON.stringify(criteriaParam) !== JSON.stringify(criteria))
|
||||
{
|
||||
const newCriteria = criteriaParam as QFilterCriteriaWithId;
|
||||
setCriteria(newCriteria);
|
||||
const operatorOption = operatorOptions.filter(o => o.value == newCriteria.operator)[0];
|
||||
setOperatorSelectedValue(operatorOption);
|
||||
setOperatorInputValue(operatorOption.label);
|
||||
if(isOpen)
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// this was firing too-often for case where: there was a criteria originally //
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
console.log("Not handling outside change (A), because dropdown is-open");
|
||||
}
|
||||
else
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// copy the criteriaParam to a new object in here - so changes won't apply until user closes the menu //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
const newCriteria = Object.assign({}, criteriaParam) as QFilterCriteriaWithId;
|
||||
setCriteria(newCriteria);
|
||||
const operatorOption = operatorOptions.filter(o => o.value == newCriteria.operator)[0];
|
||||
setOperatorSelectedValue(operatorOption);
|
||||
setOperatorInputValue(operatorOption.label);
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Test if we need to construct a new criteria object
|
||||
** This is (at least for some cases) for when the criteria gets changed
|
||||
** from outside of this component - e.g., a reset on the query screen
|
||||
*******************************************************************************/
|
||||
const criteriaNeedsReset = (): boolean =>
|
||||
{
|
||||
@ -199,6 +222,16 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
|
||||
const defaultOperatorOption = operatorOptions.filter(o => o.value == defaultOperator)[0];
|
||||
if(criteria.operator !== defaultOperatorOption?.value || JSON.stringify(criteria.values) !== JSON.stringify(getDefaultCriteriaValue()))
|
||||
{
|
||||
if(isOpen)
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
// this was firing too-often for case where: there was no criteria originally, //
|
||||
// so, by adding this is-open check, we eliminated those. //
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
console.log("Not handling outside change (B), because dropdown is-open");
|
||||
return (false);
|
||||
}
|
||||
|
||||
return (true);
|
||||
}
|
||||
}
|
||||
@ -207,7 +240,7 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Construct a new criteria object - resetting the values tied to the oprator
|
||||
** Construct a new criteria object - resetting the values tied to the operator
|
||||
** autocomplete at the same time.
|
||||
*******************************************************************************/
|
||||
const makeNewCriteria = (): QFilterCriteria =>
|
||||
@ -241,6 +274,11 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
|
||||
*******************************************************************************/
|
||||
const closeMenu = () =>
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
// when closing the menu, that's when we'll update the criteria from the caller //
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
updateCriteria(criteria, false, false);
|
||||
|
||||
setIsOpen(false);
|
||||
setAnchorEl(null);
|
||||
};
|
||||
@ -286,7 +324,8 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
|
||||
setOperatorInputValue("");
|
||||
}
|
||||
|
||||
updateCriteria(criteria, false, false);
|
||||
setCriteria(criteria);
|
||||
forceUpdate();
|
||||
};
|
||||
|
||||
/*******************************************************************************
|
||||
@ -320,7 +359,8 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
|
||||
criteria.values[valueIndex] = value;
|
||||
}
|
||||
|
||||
updateCriteria(criteria, true, false);
|
||||
setCriteria(criteria);
|
||||
forceUpdate();
|
||||
};
|
||||
|
||||
/*******************************************************************************
|
||||
@ -480,7 +520,12 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
|
||||
<Box display="inline-block" width={widthAndMaxWidth} maxWidth={widthAndMaxWidth} className="operatorColumn">
|
||||
<Autocomplete
|
||||
id={"criteriaOperator"}
|
||||
renderInput={(params) => (<TextField {...params} label={"Operator"} variant="standard" autoComplete="off" type="search" InputProps={{...params.InputProps}} />)}
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// ok, so, by default, if you type an 'o' as the first letter in the FilterCriteriaRowValues box, //
|
||||
// something is causing THIS element to become selected, if the first letter in its label is 'O'. //
|
||||
// ... work around is to put invisible ‌ entity as first character in label instead... //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
renderInput={(params) => (<TextField {...params} label={<>‌Operator</>} variant="standard" autoComplete="off" type="search" InputProps={{...params.InputProps}} />)}
|
||||
options={operatorOptions}
|
||||
value={operatorSelectedValue as any}
|
||||
inputValue={operatorInputValue}
|
||||
|
@ -28,9 +28,11 @@ import Icon from "@mui/material/Icon";
|
||||
import Tooltip from "@mui/material/Tooltip/Tooltip";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import parse from "html-react-parser";
|
||||
import React, {useEffect, useState} from "react";
|
||||
import React, {useContext, useEffect, useState} from "react";
|
||||
import {NavigateFunction, useNavigate} from "react-router-dom";
|
||||
import QContext from "QContext";
|
||||
import colors from "qqq/assets/theme/base/colors";
|
||||
import HelpContent, {hasHelpContent} from "qqq/components/misc/HelpContent";
|
||||
import WidgetDropdownMenu, {DropdownOption} from "qqq/components/widgets/components/WidgetDropdownMenu";
|
||||
import {WidgetUtils} from "qqq/components/widgets/WidgetUtils";
|
||||
import HtmlUtils from "qqq/utils/HtmlUtils";
|
||||
@ -140,7 +142,7 @@ export class HeaderIcon extends LabelComponent
|
||||
borderRadius: "0.25rem"
|
||||
};
|
||||
|
||||
if(this.role == "topLeftInsideCard")
|
||||
if (this.role == "topLeftInsideCard")
|
||||
{
|
||||
styles["order"] = -1;
|
||||
styles["marginRight"] = "0.5rem";
|
||||
@ -233,22 +235,22 @@ export class Dropdown extends LabelComponent
|
||||
try
|
||||
{
|
||||
const localStorageOption = JSON.parse(localStorage.getItem(localStorageKey));
|
||||
if(localStorageOption)
|
||||
if (localStorageOption)
|
||||
{
|
||||
const id = localStorageOption.id;
|
||||
for (let i = 0; i < this.options.length; i++)
|
||||
{
|
||||
if (this.options[i].id == id)
|
||||
{
|
||||
defaultValue = this.options[i]
|
||||
defaultValue = this.options[i];
|
||||
args.dropdownData[args.componentIndex] = defaultValue?.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(e)
|
||||
catch (e)
|
||||
{
|
||||
console.log(`Error getting default value for dropdown [${this.dropdownName}] from local storage`, e)
|
||||
console.log(`Error getting default value for dropdown [${this.dropdownName}] from local storage`, e);
|
||||
}
|
||||
}
|
||||
|
||||
@ -259,7 +261,7 @@ export class Dropdown extends LabelComponent
|
||||
{
|
||||
for (let i = 0; i < this.options.length; i++)
|
||||
{
|
||||
if(this.options[i].id == this.dropdownDefaultValue)
|
||||
if (this.options[i].id == this.dropdownDefaultValue)
|
||||
{
|
||||
defaultValue = this.options[i];
|
||||
args.dropdownData[args.componentIndex] = defaultValue?.id;
|
||||
@ -359,9 +361,11 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
||||
const [lastSeenLabel, setLastSeenLabel] = useState("");
|
||||
const [usingLabelAsTitle, setUsingLabelAsTitle] = useState(false);
|
||||
|
||||
const {helpHelpActive} = useContext(QContext);
|
||||
|
||||
function renderComponent(component: LabelComponent, componentIndex: number)
|
||||
{
|
||||
if(component && component.render)
|
||||
if (component && component.render)
|
||||
{
|
||||
return component.render({navigate: navigate, widgetProps: props, dropdownData: dropdownData, componentIndex: componentIndex, reloadFunction: doReload});
|
||||
}
|
||||
@ -397,7 +401,7 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
||||
// for initial render, put right-components from props into the state variable //
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
const stateLabelComponentsRight = [] as LabelComponent[];
|
||||
// console.log(`${props.widgetMetaData.name} init'ing right-components`);
|
||||
// console.log(`${props.widgetMetaData.name} initiating right-components`);
|
||||
if (props.labelAdditionalComponentsRight)
|
||||
{
|
||||
props.labelAdditionalComponentsRight.map((component) => stateLabelComponentsRight.push(component));
|
||||
@ -431,11 +435,11 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
||||
{
|
||||
// console.log(`${props.widgetMetaData.name} building a Dropdown, data is: ${dropdownData}`);
|
||||
let defaultValue = null;
|
||||
if(props.widgetData.dropdownDefaultValueList && props.widgetData.dropdownDefaultValueList.length >= index)
|
||||
if (props.widgetData.dropdownDefaultValueList && props.widgetData.dropdownDefaultValueList.length >= index)
|
||||
{
|
||||
defaultValue = props.widgetData.dropdownDefaultValueList[index];
|
||||
}
|
||||
if(props.widgetData?.dropdownLabelList && props.widgetData?.dropdownLabelList[index] && props.widgetMetaData?.dropdowns && props.widgetMetaData?.dropdowns[index] && props.widgetData?.dropdownNameList && props.widgetData?.dropdownNameList[index])
|
||||
if (props.widgetData?.dropdownLabelList && props.widgetData?.dropdownLabelList[index] && props.widgetMetaData?.dropdowns && props.widgetMetaData?.dropdowns[index] && props.widgetData?.dropdownNameList && props.widgetData?.dropdownNameList[index])
|
||||
{
|
||||
updatedStateLabelComponentsRight.push(new Dropdown(props.widgetData.dropdownLabelList[index], props.widgetMetaData.dropdowns[index], dropdownData, defaultValue, props.widgetData.dropdownNameList[index], handleDataChange));
|
||||
}
|
||||
@ -550,7 +554,7 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
||||
let localLabelAdditionalElementsLeft = [...props.labelAdditionalElementsLeft];
|
||||
if (props.widgetMetaData?.showExportButton && props.widgetMetaData?.type !== "table" && props.widgetMetaData?.type !== "childRecordList")
|
||||
{
|
||||
if(!localLabelAdditionalElementsLeft)
|
||||
if (!localLabelAdditionalElementsLeft)
|
||||
{
|
||||
localLabelAdditionalElementsLeft = [];
|
||||
}
|
||||
@ -585,12 +589,12 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
||||
const isParentWidget = props.widgetMetaData.type == "parentWidget"; // todo - do we need to know top-level parent, vs. a nested parent?
|
||||
let labelToUse = props.widgetData?.label ?? props.widgetMetaData?.label;
|
||||
|
||||
if(!labelToUse)
|
||||
if (!labelToUse)
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// prevent the label from disappearing, especially when it's being used as the page header //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(lastSeenLabel && isParentWidget && usingLabelAsTitle)
|
||||
if (lastSeenLabel && isParentWidget && usingLabelAsTitle)
|
||||
{
|
||||
labelToUse = lastSeenLabel;
|
||||
}
|
||||
@ -602,15 +606,32 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
||||
</Typography>
|
||||
);
|
||||
|
||||
if(labelToUse && labelToUse != lastSeenLabel)
|
||||
let sublabelElement = (
|
||||
<Box height="20px">
|
||||
<Typography sx={{position: "relative", top: "-18px"}} variant="caption">
|
||||
{props.widgetData?.sublabel}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
|
||||
if (labelToUse && labelToUse != lastSeenLabel)
|
||||
{
|
||||
setLastSeenLabel(labelToUse)
|
||||
setLastSeenLabel(labelToUse);
|
||||
setUsingLabelAsTitle(props.widgetData.isLabelPageTitle);
|
||||
}
|
||||
|
||||
if (props.widgetMetaData.tooltip)
|
||||
const helpRoles = ["ALL_SCREENS"]
|
||||
const slotName = "label";
|
||||
const showHelp = helpHelpActive || hasHelpContent(props.widgetMetaData?.helpContent?.get(slotName), helpRoles);
|
||||
|
||||
if(showHelp)
|
||||
{
|
||||
labelElement = <Tooltip title={props.widgetMetaData.tooltip} arrow={false} followCursor={true} placement="bottom-start">{labelElement}</Tooltip>;
|
||||
const formattedHelpContent = <HelpContent helpContents={props.widgetMetaData?.helpContent?.get(slotName)} roles={helpRoles} helpContentKey={`widget:${props.widgetMetaData?.name};slot:${slotName}`} />;
|
||||
labelElement = <Tooltip title={formattedHelpContent} arrow={true} placement="bottom-start">{labelElement}</Tooltip>;
|
||||
}
|
||||
else if (props.widgetMetaData?.tooltip)
|
||||
{
|
||||
labelElement = <Tooltip title={props.widgetMetaData.tooltip} arrow={true} placement="bottom-start">{labelElement}</Tooltip>;
|
||||
}
|
||||
|
||||
const isTable = props.widgetMetaData.type == "table";
|
||||
@ -621,56 +642,63 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
||||
{
|
||||
needLabelBox &&
|
||||
<Box display="flex" justifyContent="space-between" alignItems="flex-start" sx={{width: "100%", ...props.labelBoxAdditionalSx}} minHeight={"2.5rem"}>
|
||||
<Box display="flex" alignItems="baseline">
|
||||
{
|
||||
hasPermission ?
|
||||
props.widgetMetaData?.icon && (
|
||||
<Box ml={1} mr={2} mt={-4} sx={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
width: "64px",
|
||||
height: "64px",
|
||||
borderRadius: "8px",
|
||||
background: colors.info.main,
|
||||
color: "#ffffff",
|
||||
float: "left"
|
||||
}}
|
||||
>
|
||||
<Icon fontSize="medium" color="inherit">
|
||||
{props.widgetMetaData.icon}
|
||||
</Icon>
|
||||
</Box>
|
||||
) :
|
||||
(
|
||||
<Box ml={3} mt={-4} sx={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
width: "64px",
|
||||
height: "64px",
|
||||
borderRadius: "8px",
|
||||
background: colors.info.main,
|
||||
color: "#ffffff",
|
||||
float: "left"
|
||||
}}
|
||||
>
|
||||
<Icon fontSize="medium" color="inherit">lock</Icon>
|
||||
</Box>
|
||||
<Box display="flex" flexDirection="column">
|
||||
<Box display="flex" alignItems="baseline">
|
||||
{
|
||||
hasPermission ?
|
||||
props.widgetMetaData?.icon && (
|
||||
<Box ml={1} mr={2} mt={-4} sx={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
width: "64px",
|
||||
height: "64px",
|
||||
borderRadius: "8px",
|
||||
background: colors.info.main,
|
||||
color: "#ffffff",
|
||||
float: "left"
|
||||
}}
|
||||
>
|
||||
<Icon fontSize="medium" color="inherit">
|
||||
{props.widgetMetaData.icon}
|
||||
</Icon>
|
||||
</Box>
|
||||
) :
|
||||
(
|
||||
<Box ml={3} mt={-4} sx={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
width: "64px",
|
||||
height: "64px",
|
||||
borderRadius: "8px",
|
||||
background: colors.info.main,
|
||||
color: "#ffffff",
|
||||
float: "left"
|
||||
}}
|
||||
>
|
||||
<Icon fontSize="medium" color="inherit">lock</Icon>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
{
|
||||
hasPermission && labelToUse && (labelElement)
|
||||
}
|
||||
{
|
||||
hasPermission && (
|
||||
labelComponentsLeft.map((component, i) =>
|
||||
{
|
||||
return (<React.Fragment key={i}>{renderComponent(component, i)}</React.Fragment>);
|
||||
})
|
||||
)
|
||||
}
|
||||
{
|
||||
hasPermission && labelToUse && (labelElement)
|
||||
}
|
||||
{
|
||||
hasPermission && (
|
||||
labelComponentsLeft.map((component, i) =>
|
||||
{
|
||||
return (<React.Fragment key={i}>{renderComponent(component, i)}</React.Fragment>);
|
||||
})
|
||||
)
|
||||
}
|
||||
{localLabelAdditionalElementsLeft}
|
||||
}
|
||||
{localLabelAdditionalElementsLeft}
|
||||
</Box>
|
||||
<Box display="flex">
|
||||
{
|
||||
hasPermission && props.widgetData?.sublabel && (sublabelElement)
|
||||
}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box>
|
||||
{
|
||||
@ -727,7 +755,7 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
||||
// try to make tables fill their entire "parent" //
|
||||
///////////////////////////////////////////////////
|
||||
let noCardMarginBottom = "unset";
|
||||
if(isTable)
|
||||
if (isTable)
|
||||
{
|
||||
noCardMarginBottom = "-8px";
|
||||
}
|
||||
|
@ -39,7 +39,7 @@ export default function BigNumberBlock({widgetMetaData, data}: StandardBlockComp
|
||||
return (
|
||||
<div style={{width: data.styles.width ?? "auto"}}>
|
||||
|
||||
<div style={{fontWeight: "700", fontSize: "0.875rem", color: "#3D3D3D"}}>
|
||||
<div style={{fontWeight: "700", fontSize: "0.875rem", color: "#3D3D3D", marginBottom: "-0.5rem"}}>
|
||||
<BlockElementWrapper data={data} slot="heading">
|
||||
<span>{data.values.heading}</span>
|
||||
</BlockElementWrapper>
|
||||
|
@ -39,65 +39,79 @@ ChartJS.register(
|
||||
Legend
|
||||
);
|
||||
|
||||
export const options = {
|
||||
maintainAspectRatio: false,
|
||||
responsive: true,
|
||||
animation: {
|
||||
duration: 0
|
||||
},
|
||||
elements: {
|
||||
bar: {
|
||||
borderRadius: 4
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
tooltip: {
|
||||
// todo - some configs around this
|
||||
callbacks: {
|
||||
title: function(context: any)
|
||||
{
|
||||
return ("");
|
||||
},
|
||||
label: function(context: any)
|
||||
{
|
||||
if(context.dataset.label.startsWith(context.label))
|
||||
{
|
||||
return `${context.label}: ${context.formattedValue}`;
|
||||
}
|
||||
else
|
||||
export const makeOptions = (data: DefaultChartData) =>
|
||||
{
|
||||
return({
|
||||
maintainAspectRatio: false,
|
||||
responsive: true,
|
||||
animation: {
|
||||
duration: 0
|
||||
},
|
||||
elements: {
|
||||
bar: {
|
||||
borderRadius: 4
|
||||
}
|
||||
},
|
||||
onHover: function (event: any, elements: any[], chart: any)
|
||||
{
|
||||
if(event.type == "mousemove" && elements.length > 0 && data.urls && data.urls.length > elements[0].index && data.urls[elements[0].index])
|
||||
{
|
||||
chart.canvas.style.cursor = "pointer";
|
||||
}
|
||||
else
|
||||
{
|
||||
chart.canvas.style.cursor = "default";
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
tooltip: {
|
||||
// todo - some configs around this
|
||||
callbacks: {
|
||||
title: function(context: any)
|
||||
{
|
||||
return ("");
|
||||
},
|
||||
label: function(context: any)
|
||||
{
|
||||
if(context.dataset.label.startsWith(context.label))
|
||||
{
|
||||
return `${context.label}: ${context.formattedValue}`;
|
||||
}
|
||||
else
|
||||
{
|
||||
return ("");
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
position: "bottom",
|
||||
labels: {
|
||||
usePointStyle: true,
|
||||
pointStyle: "circle",
|
||||
boxHeight: 6,
|
||||
boxWidth: 6,
|
||||
padding: 12,
|
||||
font: {
|
||||
size: 14
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
position: "bottom",
|
||||
labels: {
|
||||
usePointStyle: true,
|
||||
pointStyle: "circle",
|
||||
boxHeight: 6,
|
||||
boxWidth: 6,
|
||||
padding: 12,
|
||||
font: {
|
||||
size: 14
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
stacked: true,
|
||||
grid: {display: false},
|
||||
ticks: {autoSkip: false, maxRotation: 90}
|
||||
scales: {
|
||||
x: {
|
||||
stacked: true,
|
||||
grid: {display: false},
|
||||
ticks: {autoSkip: false, maxRotation: 90}
|
||||
},
|
||||
y: {
|
||||
stacked: true,
|
||||
position: "right",
|
||||
ticks: {precision: 0}
|
||||
},
|
||||
},
|
||||
y: {
|
||||
stacked: true,
|
||||
position: "right",
|
||||
ticks: {precision: 0}
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
interface Props
|
||||
{
|
||||
@ -151,7 +165,7 @@ function StackedBarChart({data, chartSubheaderData}: Props): JSX.Element
|
||||
<Box>
|
||||
{chartSubheaderData && (<ChartSubheaderWithData chartSubheaderData={chartSubheaderData} />)}
|
||||
<Box width="100%" height="300px">
|
||||
<Bar data={data} options={options} getElementsAtEvent={handleClick} />
|
||||
<Bar data={data} options={makeOptions(data)} getElementsAtEvent={handleClick} />
|
||||
</Box>
|
||||
</Box>
|
||||
) : <Skeleton sx={{marginLeft: "20px", marginRight: "20px", height: "200px"}} />;
|
||||
|
@ -70,7 +70,7 @@ function PieChart({description, chartData, chartSubheaderData}: Props): JSX.Elem
|
||||
chartData.dataset.backgroundColors = chartColors;
|
||||
}
|
||||
}
|
||||
const {data, options} = configs(chartData?.labels || [], chartData?.dataset || {});
|
||||
const {data, options} = configs(chartData?.labels || [], chartData?.dataset || {}, chartData?.dataset?.urls);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
|
@ -23,7 +23,7 @@ import colors from "qqq/assets/theme/base/colors";
|
||||
|
||||
const {gradients, dark} = colors;
|
||||
|
||||
function configs(labels: any, datasets: any)
|
||||
function configs(labels: any, datasets: any, urls: string[] | undefined)
|
||||
{
|
||||
const backgroundColors = [];
|
||||
|
||||
@ -66,6 +66,17 @@ function configs(labels: any, datasets: any)
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
responsive: true,
|
||||
onHover: function (event: any, elements: any[], chart: any)
|
||||
{
|
||||
if(event.type == "mousemove" && elements.length > 0 && urls && urls.length > elements[0].index && urls[elements[0].index])
|
||||
{
|
||||
chart.canvas.style.cursor = "pointer";
|
||||
}
|
||||
else
|
||||
{
|
||||
chart.canvas.style.cursor = "default";
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
|
@ -30,8 +30,8 @@ import TableContainer from "@mui/material/TableContainer";
|
||||
import TableRow from "@mui/material/TableRow";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import parse from "html-react-parser";
|
||||
import {useEffect, useMemo, useState} from "react";
|
||||
import {useAsyncDebounce, useGlobalFilter, usePagination, useSortBy, useTable, useExpanded} from "react-table";
|
||||
import React, {useEffect, useMemo, useState} from "react";
|
||||
import {useAsyncDebounce, useExpanded, useGlobalFilter, usePagination, useSortBy, useTable} from "react-table";
|
||||
import colors from "qqq/assets/theme/base/colors";
|
||||
import MDInput from "qqq/components/legacy/MDInput";
|
||||
import MDPagination from "qqq/components/legacy/MDPagination";
|
||||
@ -173,6 +173,17 @@ function DataTable({
|
||||
);
|
||||
}
|
||||
|
||||
if(table.columnHeaderTooltips)
|
||||
{
|
||||
for (let column of columnsToMemo)
|
||||
{
|
||||
if(table.columnHeaderTooltips[column.accessor])
|
||||
{
|
||||
column.tooltip = table.columnHeaderTooltips[column.accessor];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const columns = useMemo<any>(() => columnsToMemo, [table]);
|
||||
const data = useMemo<any>(() => table.rows, [table]);
|
||||
const gridTemplateColumns = widths.join(" ");
|
||||
@ -324,6 +335,7 @@ function DataTable({
|
||||
{...column.getHeaderProps(isSorted && column.getSortByToggleProps())}
|
||||
align={column.align ? column.align : "left"}
|
||||
sorted={setSortedValue(column)}
|
||||
tooltip={column.tooltip}
|
||||
>
|
||||
{column.render("header")}
|
||||
</DataTableHeadCell>
|
||||
|
@ -43,6 +43,7 @@ import Client from "qqq/utils/qqq/Client";
|
||||
export interface TableDataInput
|
||||
{
|
||||
columns: { [key: string]: any }[];
|
||||
columnHeaderTooltips?: { [columnName: string]: string | JSX.Element }
|
||||
rows: { [key: string]: any }[];
|
||||
}
|
||||
|
||||
|
@ -23,7 +23,9 @@
|
||||
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
||||
// @ts-ignore
|
||||
import {htmlToText} from "html-to-text";
|
||||
import React, {useEffect, useState} from "react";
|
||||
import React, {useContext, useEffect, useState} from "react";
|
||||
import QContext from "QContext";
|
||||
import HelpContent, {hasHelpContent} from "qqq/components/misc/HelpContent";
|
||||
import TableCard from "qqq/components/widgets/tables/TableCard";
|
||||
import Widget, {WidgetData} from "qqq/components/widgets/Widget";
|
||||
import {WidgetUtils} from "qqq/components/widgets/WidgetUtils";
|
||||
@ -46,6 +48,7 @@ function TableWidget(props: Props): JSX.Element
|
||||
const [isExportDisabled, setIsExportDisabled] = useState(false); // hmm, would like true here, but it broke...
|
||||
const [csv, setCsv] = useState(null as string);
|
||||
const [fileName, setFileName] = useState(null as string);
|
||||
const {helpHelpActive} = useContext(QContext);
|
||||
|
||||
const rows = props.widgetData?.rows;
|
||||
const columns = props.widgetData?.columns;
|
||||
@ -133,6 +136,23 @@ function TableWidget(props: Props): JSX.Element
|
||||
labelAdditionalElementsLeft.push(WidgetUtils.generateExportButton(onExportClick));
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////
|
||||
// look for column-header tooltips from helpContent //
|
||||
//////////////////////////////////////////////////////
|
||||
const columnHeaderTooltips: {[columnName: string]: JSX.Element} = {}
|
||||
for (let column of props.widgetData?.columns ?? [])
|
||||
{
|
||||
const helpRoles = ["ALL_SCREENS"]
|
||||
const slotName = `columnHeader=${column.accessor}`;
|
||||
const showHelp = helpHelpActive || hasHelpContent(props.widgetMetaData?.helpContent?.get(slotName), helpRoles);
|
||||
|
||||
if(showHelp)
|
||||
{
|
||||
const formattedHelpContent = <HelpContent helpContents={props.widgetMetaData?.helpContent?.get(slotName)} roles={helpRoles} helpContentKey={`widget:${props.widgetMetaData?.name};slot:${slotName}`} />;
|
||||
columnHeaderTooltips[column.accessor] = formattedHelpContent;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget
|
||||
widgetMetaData={props.widgetMetaData}
|
||||
@ -148,7 +168,7 @@ function TableWidget(props: Props): JSX.Element
|
||||
hidePaginationDropdown={props.widgetData?.hidePaginationDropdown}
|
||||
fixedStickyLastRow={props.widgetData?.fixedStickyLastRow}
|
||||
fixedHeight={props.widgetData?.fixedHeight}
|
||||
data={{columns: props.widgetData?.columns, rows: props.widgetData?.rows}}
|
||||
data={{columns: props.widgetData?.columns, rows: props.widgetData?.rows, columnHeaderTooltips: columnHeaderTooltips}}
|
||||
widgetMetaData={props.widgetMetaData}
|
||||
/>
|
||||
</Widget>
|
||||
|
@ -22,6 +22,7 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import Icon from "@mui/material/Icon";
|
||||
import {Theme} from "@mui/material/styles";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import {ReactNode} from "react";
|
||||
import colors from "qqq/assets/theme/base/colors";
|
||||
import {useMaterialUIController} from "qqq/context";
|
||||
@ -33,9 +34,10 @@ interface Props
|
||||
children: ReactNode;
|
||||
sorted?: false | "none" | "asce" | "desc";
|
||||
align?: "left" | "right" | "center";
|
||||
tooltip?: string | JSX.Element;
|
||||
}
|
||||
|
||||
function DataTableHeadCell({width, children, sorted, align, ...rest}: Props): JSX.Element
|
||||
function DataTableHeadCell({width, children, sorted, align, tooltip, ...rest}: Props): JSX.Element
|
||||
{
|
||||
const [controller] = useMaterialUIController();
|
||||
const {darkMode} = controller;
|
||||
@ -73,39 +75,43 @@ function DataTableHeadCell({width, children, sorted, align, ...rest}: Props): JS
|
||||
userSelect: sorted && "none",
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
{sorted && (
|
||||
<Box
|
||||
position="absolute"
|
||||
top={0}
|
||||
right={align !== "right" ? "16px" : 0}
|
||||
left={align === "right" ? "-5px" : "unset"}
|
||||
sx={({typography: {size}}: any) => ({
|
||||
fontSize: size.lg,
|
||||
})}
|
||||
>
|
||||
<>
|
||||
{
|
||||
tooltip ? <Tooltip title={tooltip}><span style={{cursor: "default"}}>{children}</span></Tooltip> : children
|
||||
}
|
||||
{sorted && (
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: -6,
|
||||
color: sorted === "asce" ? "text" : "secondary",
|
||||
opacity: sorted === "asce" ? 1 : 0.5
|
||||
}}
|
||||
position="absolute"
|
||||
top={0}
|
||||
right={align !== "right" ? "16px" : 0}
|
||||
left={align === "right" ? "-5px" : "unset"}
|
||||
sx={({typography: {size}}: any) => ({
|
||||
fontSize: size.lg,
|
||||
})}
|
||||
>
|
||||
<Icon>arrow_drop_up</Icon>
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: -6,
|
||||
color: sorted === "asce" ? "text" : "secondary",
|
||||
opacity: sorted === "asce" ? 1 : 0.5
|
||||
}}
|
||||
>
|
||||
<Icon>arrow_drop_up</Icon>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
color: sorted === "desc" ? "text" : "secondary",
|
||||
opacity: sorted === "desc" ? 1 : 0.5
|
||||
}}
|
||||
>
|
||||
<Icon>arrow_drop_down</Icon>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
color: sorted === "desc" ? "text" : "secondary",
|
||||
opacity: sorted === "desc" ? 1 : 0.5
|
||||
}}
|
||||
>
|
||||
<Icon>arrow_drop_down</Icon>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
)}
|
||||
</>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
@ -168,7 +168,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
||||
// define some default values (e.g., to be used if nothing in local storage or no active view) //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
let defaultSort = [] as GridSortItem[];
|
||||
let defaultRowsPerPage = 10;
|
||||
let defaultRowsPerPage = 50;
|
||||
let defaultDensity = "standard" as GridDensity;
|
||||
let defaultTableVariant: QTableVariant = null;
|
||||
let defaultMode = "basic";
|
||||
@ -610,11 +610,14 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
||||
e.preventDefault()
|
||||
updateTable("'r' keyboard event");
|
||||
}
|
||||
/*
|
||||
// disable until we add a ... ref down to let us programmatically open Columns button
|
||||
else if (! e.metaKey && e.key === "c")
|
||||
{
|
||||
e.preventDefault()
|
||||
gridApiRef.current.showPreferences(GridPreferencePanelsValue.columns)
|
||||
}
|
||||
*/
|
||||
else if (! e.metaKey && e.key === "f")
|
||||
{
|
||||
e.preventDefault()
|
||||
@ -1380,7 +1383,10 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
||||
{
|
||||
if (selectFullFilterState === "filter")
|
||||
{
|
||||
return `?recordsParam=filterJSON&filterJSON=${encodeURIComponent(JSON.stringify(prepQueryFilterForBackend(queryFilter)))}`;
|
||||
const filterForBackend = prepQueryFilterForBackend(queryFilter);
|
||||
filterForBackend.skip = 0;
|
||||
filterForBackend.limit = null;
|
||||
return `?recordsParam=filterJSON&filterJSON=${encodeURIComponent(JSON.stringify(filterForBackend))}`;
|
||||
}
|
||||
|
||||
if (selectFullFilterState === "filterSubset")
|
||||
@ -1408,7 +1414,10 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
||||
{
|
||||
if (selectFullFilterState === "filter")
|
||||
{
|
||||
setRecordIdsForProcess(prepQueryFilterForBackend(queryFilter));
|
||||
const filterForBackend = prepQueryFilterForBackend(queryFilter);
|
||||
filterForBackend.skip = 0;
|
||||
filterForBackend.limit = null;
|
||||
setRecordIdsForProcess(filterForBackend);
|
||||
}
|
||||
else if (selectFullFilterState === "filterSubset")
|
||||
{
|
||||
@ -2103,20 +2112,32 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
||||
{
|
||||
if(selectedIndex == 0)
|
||||
{
|
||||
///////////////
|
||||
// this page //
|
||||
///////////////
|
||||
programmaticallySelectSomeOrAllRows();
|
||||
setSelectFullFilterState("checked")
|
||||
}
|
||||
else if(selectedIndex == 1)
|
||||
{
|
||||
///////////////////////
|
||||
// full query result //
|
||||
///////////////////////
|
||||
programmaticallySelectSomeOrAllRows();
|
||||
setSelectFullFilterState("filter")
|
||||
}
|
||||
else if(selectedIndex == 2)
|
||||
{
|
||||
////////////////////////////
|
||||
// subset of query result //
|
||||
////////////////////////////
|
||||
setSelectionSubsetSizePromptOpen(true);
|
||||
}
|
||||
else if(selectedIndex == 3)
|
||||
{
|
||||
/////////////////////
|
||||
// clear selection //
|
||||
/////////////////////
|
||||
setSelectFullFilterState("n/a")
|
||||
setRowSelectionModel([]);
|
||||
setSelectedIds([]);
|
||||
|
@ -64,7 +64,6 @@ class FilterUtils
|
||||
let rs = [];
|
||||
for (let i = 0; i < param.length; i++)
|
||||
{
|
||||
console.log(param[i]);
|
||||
if (param[i] && param[i].id && param[i].label)
|
||||
{
|
||||
/////////////////////////////////////////////////////////////
|
||||
@ -361,7 +360,12 @@ class FilterUtils
|
||||
else if (value.type == "ThisOrLastPeriod")
|
||||
{
|
||||
const expression = new ThisOrLastPeriodExpression(value);
|
||||
labels.push(expression.toString());
|
||||
let startOfPrefix = "";
|
||||
if(fieldMetaData.type == QFieldType.DATE_TIME || expression.timeUnit != "DAYS")
|
||||
{
|
||||
startOfPrefix = "start of ";
|
||||
}
|
||||
labels.push(`${startOfPrefix}${expression.toString()}`);
|
||||
}
|
||||
else if(fieldMetaData.type == QFieldType.BOOLEAN)
|
||||
{
|
||||
|
Reference in New Issue
Block a user