mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-17 12:50:43 +00:00
New style & functionality (null-label, default-from-data) for widget dropdowns
This commit is contained in:
@ -25,14 +25,13 @@ import Box from "@mui/material/Box";
|
||||
import Button from "@mui/material/Button";
|
||||
import Card from "@mui/material/Card";
|
||||
import Icon from "@mui/material/Icon";
|
||||
import LinearProgress from "@mui/material/LinearProgress";
|
||||
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 {NavigateFunction, useNavigate} from "react-router-dom";
|
||||
import colors from "qqq/assets/theme/base/colors";
|
||||
import DropdownMenu, {DropdownOption} from "qqq/components/widgets/components/DropdownMenu";
|
||||
import WidgetDropdownMenu, {DropdownOption} from "qqq/components/widgets/components/WidgetDropdownMenu";
|
||||
|
||||
export interface WidgetData
|
||||
{
|
||||
@ -43,6 +42,7 @@ export interface WidgetData
|
||||
id: string,
|
||||
label: string
|
||||
}[][];
|
||||
dropdownDefaultValueList?: string[];
|
||||
dropdownNeedsSelectedText?: string;
|
||||
hasPermission?: boolean;
|
||||
errorLoading?: boolean;
|
||||
@ -134,15 +134,15 @@ export class HeaderIcon extends LabelComponent
|
||||
borderRadius: "0.25rem"
|
||||
};
|
||||
|
||||
if(this.iconPath)
|
||||
if (this.iconPath)
|
||||
{
|
||||
return (<Box sx={{textAlign: "center", ...styles}}><img src={this.iconPath} width="16" height="16" /></Box>)
|
||||
return (<Box sx={{textAlign: "center", ...styles}}><img src={this.iconPath} width="16" height="16" /></Box>);
|
||||
}
|
||||
else
|
||||
{
|
||||
return (<Icon sx={{padding: "0.25rem", ...styles}} fontSize="small">{this.iconName}</Icon>);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -188,41 +188,111 @@ export class AddNewRecordButton extends LabelComponent
|
||||
export class Dropdown extends LabelComponent
|
||||
{
|
||||
label: string;
|
||||
dropdownMetaData: any;
|
||||
options: DropdownOption[];
|
||||
dropdownDefaultValue?: string;
|
||||
dropdownName: string;
|
||||
onChangeCallback: any;
|
||||
|
||||
constructor(label: string, options: DropdownOption[], dropdownName: string, onChangeCallback: any)
|
||||
constructor(label: string, dropdownMetaData: any, options: DropdownOption[], dropdownDefaultValue: string, dropdownName: string, onChangeCallback: any)
|
||||
{
|
||||
super();
|
||||
this.label = label;
|
||||
this.dropdownMetaData = dropdownMetaData;
|
||||
this.options = options;
|
||||
this.dropdownDefaultValue = dropdownDefaultValue;
|
||||
this.dropdownName = dropdownName;
|
||||
this.onChangeCallback = onChangeCallback;
|
||||
}
|
||||
|
||||
render = (args: LabelComponentRenderArgs): JSX.Element =>
|
||||
{
|
||||
const label = `Select ${this.label}`;
|
||||
let defaultValue = null;
|
||||
const localStorageKey = `${WIDGET_DROPDOWN_SELECTION_LOCAL_STORAGE_KEY_ROOT}.${args.widgetProps.widgetMetaData.name}.${this.dropdownName}`;
|
||||
if (args.widgetProps.storeDropdownSelections)
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
// see if an existing value is stored in local storage, and if so set it in dropdown //
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
defaultValue = JSON.parse(localStorage.getItem(localStorageKey));
|
||||
args.dropdownData[args.componentIndex] = defaultValue?.id;
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// see if an existing value is stored in local storage, and if so set it in dropdown //
|
||||
// originally we used the full object from localStorage - but - in case the label //
|
||||
// changed since it was stored, we'll instead just find the option by id (or in case that //
|
||||
// option isn't available anymore, then we'll select nothing instead of a missing value //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
try
|
||||
{
|
||||
const localStorageOption = JSON.parse(localStorage.getItem(localStorageKey));
|
||||
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]
|
||||
args.dropdownData[args.componentIndex] = defaultValue?.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
console.log(`Error getting default value for dropdown [${this.dropdownName}] from local storage`, e)
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if there wasn't a value selected, but there is a default from the backend, then use it. //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if (defaultValue == null && this.dropdownDefaultValue != null)
|
||||
{
|
||||
for (let i = 0; i < this.options.length; i++)
|
||||
{
|
||||
if(this.options[i].id == this.dropdownDefaultValue)
|
||||
{
|
||||
defaultValue = this.options[i];
|
||||
args.dropdownData[args.componentIndex] = defaultValue?.id;
|
||||
|
||||
if (args.widgetProps.storeDropdownSelections)
|
||||
{
|
||||
localStorage.setItem(localStorageKey, JSON.stringify(defaultValue));
|
||||
}
|
||||
|
||||
this.onChangeCallback(label, defaultValue);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// if there's a 'label for null value' (and no default from the backend), //
|
||||
// then add that as an option (and select it if nothing else was selected) //
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
let options = this.options;
|
||||
if (this.dropdownMetaData.labelForNullValue && !this.dropdownDefaultValue)
|
||||
{
|
||||
const nullOption = {id: null as string, label: this.dropdownMetaData.labelForNullValue};
|
||||
options = [nullOption, ...this.options];
|
||||
|
||||
if (!defaultValue)
|
||||
{
|
||||
defaultValue = nullOption;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Box my={2} sx={{float: "right"}}>
|
||||
<DropdownMenu
|
||||
<WidgetDropdownMenu
|
||||
name={this.dropdownName}
|
||||
defaultValue={defaultValue}
|
||||
sx={{width: 200, marginLeft: "15px"}}
|
||||
label={`Select ${this.label}`}
|
||||
dropdownOptions={this.options}
|
||||
sx={{marginLeft: "1rem"}}
|
||||
label={label}
|
||||
startIcon={this.dropdownMetaData.startIconName}
|
||||
allowBackAndForth={this.dropdownMetaData.allowBackAndForth}
|
||||
backAndForthInverted={this.dropdownMetaData.backAndForthInverted}
|
||||
disableClearable={this.dropdownMetaData.disableClearable}
|
||||
dropdownOptions={options}
|
||||
onChangeCallback={this.onChangeCallback}
|
||||
width={this.dropdownMetaData.width ?? 225}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
@ -332,7 +402,12 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
||||
props.widgetData.dropdownDataList?.map((dropdownData: any, index: number) =>
|
||||
{
|
||||
// console.log(`${props.widgetMetaData.name} building a Dropdown, data is: ${dropdownData}`);
|
||||
updatedStateLabelComponentsRight.push(new Dropdown(props.widgetData.dropdownLabelList[index], dropdownData, props.widgetData.dropdownNameList[index], handleDataChange));
|
||||
let defaultValue = null;
|
||||
if(props.widgetData.dropdownDefaultValueList && props.widgetData.dropdownDefaultValueList.length >= index)
|
||||
{
|
||||
defaultValue = props.widgetData.dropdownDefaultValueList[index];
|
||||
}
|
||||
updatedStateLabelComponentsRight.push(new Dropdown(props.widgetData.dropdownLabelList[index], props.widgetMetaData.dropdowns[index], dropdownData, defaultValue, props.widgetData.dropdownNameList[index], handleDataChange));
|
||||
});
|
||||
setLabelComponentsRight(updatedStateLabelComponentsRight);
|
||||
}
|
||||
@ -460,16 +535,16 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
||||
// first look for a label in the widget data, which would override that in the metadata //
|
||||
// note - previously this had a ?: and one was pl={2}, the other was pl={3}... //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
const labelToUse = props.widgetData?.label ?? props.widgetMetaData?.label
|
||||
const labelToUse = props.widgetData?.label ?? props.widgetMetaData?.label;
|
||||
let labelElement = (
|
||||
<Typography sx={{cursor: "default", pl: "auto", pt: props.widgetMetaData.type == "parentWidget" ? "1rem" : "auto", fontWeight: 600}} variant="h6" display="inline">
|
||||
{labelToUse}
|
||||
</Typography>
|
||||
);
|
||||
|
||||
if(props.widgetMetaData.tooltip)
|
||||
if (props.widgetMetaData.tooltip)
|
||||
{
|
||||
labelElement = <Tooltip title={props.widgetMetaData.tooltip} arrow={false} followCursor={true} placement="bottom-start">{labelElement}</Tooltip>
|
||||
labelElement = <Tooltip title={props.widgetMetaData.tooltip} arrow={false} followCursor={true} placement="bottom-start">{labelElement}</Tooltip>;
|
||||
}
|
||||
|
||||
const errorLoading = props.widgetData?.errorLoading !== undefined && props.widgetData?.errorLoading === true;
|
||||
@ -578,7 +653,7 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
||||
}
|
||||
</Box>;
|
||||
|
||||
const padding = props.omitPadding? "auto" : "24px 16px";
|
||||
const padding = props.omitPadding ? "auto" : "24px 16px";
|
||||
return props.widgetMetaData?.isCard
|
||||
? <Card sx={{marginTop: props.widgetMetaData?.icon ? 2 : 0, width: "100%", p: padding}} className={fullScreenWidgetClassName}>
|
||||
{widgetContent}
|
||||
|
@ -1,179 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2022. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Collapse, Theme} from "@mui/material";
|
||||
import Autocomplete from "@mui/material/Autocomplete";
|
||||
import Box from "@mui/material/Box";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import {SxProps} from "@mui/system";
|
||||
import {Field, Form, Formik} from "formik";
|
||||
import React, {useState} from "react";
|
||||
import MDInput from "qqq/components/legacy/MDInput";
|
||||
import FilterUtils from "qqq/utils/qqq/FilterUtils";
|
||||
import ValueUtils from "qqq/utils/qqq/ValueUtils";
|
||||
|
||||
|
||||
export interface DropdownOption
|
||||
{
|
||||
id: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
/////////////////////////
|
||||
// inputs and defaults //
|
||||
/////////////////////////
|
||||
interface Props
|
||||
{
|
||||
name: string;
|
||||
defaultValue?: any;
|
||||
label?: string;
|
||||
dropdownOptions?: DropdownOption[];
|
||||
onChangeCallback?: (dropdownLabel: string, data: any) => void;
|
||||
sx?: SxProps<Theme>;
|
||||
}
|
||||
|
||||
interface StartAndEndDate
|
||||
{
|
||||
startDate?: string,
|
||||
endDate?: string
|
||||
}
|
||||
|
||||
function parseCustomTimeValuesFromDefaultValue(defaultValue: any): StartAndEndDate
|
||||
{
|
||||
const customTimeValues: StartAndEndDate = {};
|
||||
if(defaultValue && defaultValue.id)
|
||||
{
|
||||
var parts = defaultValue.id.split(",");
|
||||
if(parts.length >= 2)
|
||||
{
|
||||
customTimeValues["startDate"] = ValueUtils.formatDateTimeValueForForm(parts[1]);
|
||||
}
|
||||
if(parts.length >= 3)
|
||||
{
|
||||
customTimeValues["endDate"] = ValueUtils.formatDateTimeValueForForm(parts[2]);
|
||||
}
|
||||
}
|
||||
|
||||
return (customTimeValues);
|
||||
}
|
||||
|
||||
function makeBackendValuesFromFrontendValues(frontendDefaultValues: StartAndEndDate): StartAndEndDate
|
||||
{
|
||||
const backendTimeValues: StartAndEndDate = {};
|
||||
if(frontendDefaultValues && frontendDefaultValues.startDate)
|
||||
{
|
||||
backendTimeValues.startDate = ValueUtils.frontendLocalZoneDateTimeStringToUTCStringForBackend(frontendDefaultValues.startDate);
|
||||
}
|
||||
if(frontendDefaultValues && frontendDefaultValues.endDate)
|
||||
{
|
||||
backendTimeValues.endDate = ValueUtils.frontendLocalZoneDateTimeStringToUTCStringForBackend(frontendDefaultValues.endDate);
|
||||
}
|
||||
return (backendTimeValues);
|
||||
}
|
||||
|
||||
function DropdownMenu({name, defaultValue, label, dropdownOptions, onChangeCallback, sx}: Props): JSX.Element
|
||||
{
|
||||
const [customTimesVisible, setCustomTimesVisible] = useState(defaultValue && defaultValue.id && defaultValue.id.startsWith("custom,"));
|
||||
const [customTimeValuesFrontend, setCustomTimeValuesFrontend] = useState(parseCustomTimeValuesFromDefaultValue(defaultValue) as StartAndEndDate);
|
||||
const [customTimeValuesBackend, setCustomTimeValuesBackend] = useState(makeBackendValuesFromFrontendValues(customTimeValuesFrontend) as StartAndEndDate);
|
||||
const [debounceTimeout, setDebounceTimeout] = useState(null as any);
|
||||
|
||||
const handleOnChange = (event: any, newValue: any, reason: string) =>
|
||||
{
|
||||
const isTimeframeCustom = name == "timeframe" && newValue && newValue.id == "custom"
|
||||
setCustomTimesVisible(isTimeframeCustom);
|
||||
|
||||
if(isTimeframeCustom)
|
||||
{
|
||||
callOnChangeCallbackIfCustomTimeframeHasDateValues();
|
||||
}
|
||||
else
|
||||
{
|
||||
onChangeCallback(label, newValue);
|
||||
}
|
||||
};
|
||||
|
||||
const callOnChangeCallbackIfCustomTimeframeHasDateValues = () =>
|
||||
{
|
||||
if(customTimeValuesBackend["startDate"] && customTimeValuesBackend["endDate"])
|
||||
{
|
||||
onChangeCallback(label, {id: `custom,${customTimeValuesBackend["startDate"]},${customTimeValuesBackend["endDate"]}`, label: "Custom"});
|
||||
}
|
||||
}
|
||||
|
||||
let customTimes = <></>;
|
||||
if (name == "timeframe")
|
||||
{
|
||||
const handleSubmit = async (values: any, actions: any) =>
|
||||
{
|
||||
};
|
||||
|
||||
const dateChanged = (fieldName: "startDate" | "endDate", event: any) =>
|
||||
{
|
||||
customTimeValuesFrontend[fieldName] = event.target.value;
|
||||
customTimeValuesBackend[fieldName] = ValueUtils.frontendLocalZoneDateTimeStringToUTCStringForBackend(event.target.value);
|
||||
|
||||
clearTimeout(debounceTimeout);
|
||||
const newDebounceTimeout = setTimeout(() =>
|
||||
{
|
||||
callOnChangeCallbackIfCustomTimeframeHasDateValues();
|
||||
}, 500);
|
||||
setDebounceTimeout(newDebounceTimeout);
|
||||
};
|
||||
|
||||
customTimes = <Box sx={{display: "inline-block", position: "relative", top: "-7px"}}>
|
||||
<Collapse orientation="horizontal" in={customTimesVisible}>
|
||||
<Formik initialValues={customTimeValuesFrontend} onSubmit={handleSubmit}>
|
||||
{({}) => (
|
||||
<Form id="timeframe-form" autoComplete="off">
|
||||
<Field name="startDate" type="datetime-local" as={MDInput} variant="standard" label="Custom Timeframe Start" InputLabelProps={{shrink: true}} InputProps={{size: "small"}} sx={{ml: 2, width: 198}} onChange={(event: any) => dateChanged("startDate", event)} />
|
||||
<Field name="endDate" type="datetime-local" as={MDInput} variant="standard" label="Custom Timeframe End" InputLabelProps={{shrink: true}} InputProps={{size: "small"}} sx={{ml: 2, width: 198}} onChange={(event: any) => dateChanged("endDate", event)} />
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</Collapse>
|
||||
</Box>;
|
||||
}
|
||||
|
||||
return (
|
||||
dropdownOptions ? (
|
||||
<span style={{whiteSpace: "nowrap", display: "flex"}} className="dashboardDropdownMenu">
|
||||
<Autocomplete
|
||||
defaultValue={defaultValue}
|
||||
size="small"
|
||||
disablePortal
|
||||
id={`${label}-combo-box`}
|
||||
options={dropdownOptions}
|
||||
sx={{...sx, cursor: "pointer", display: "inline-block"}}
|
||||
onChange={handleOnChange}
|
||||
isOptionEqualToValue={(option, value) => option.id === value.id}
|
||||
renderInput={(params: any) => <TextField {...params} label={label} />}
|
||||
renderOption={(props, option: DropdownOption) => (
|
||||
<li {...props} style={{whiteSpace: "normal"}}>{option.label}</li>
|
||||
)}
|
||||
/>
|
||||
{customTimes}
|
||||
</span>
|
||||
) : null
|
||||
);
|
||||
}
|
||||
|
||||
export default DropdownMenu;
|
333
src/qqq/components/widgets/components/WidgetDropdownMenu.tsx
Normal file
333
src/qqq/components/widgets/components/WidgetDropdownMenu.tsx
Normal file
@ -0,0 +1,333 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2022. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Collapse, Theme, InputAdornment} from "@mui/material";
|
||||
import Autocomplete from "@mui/material/Autocomplete";
|
||||
import Box from "@mui/material/Box";
|
||||
import Icon from "@mui/material/Icon";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import {SxProps} from "@mui/system";
|
||||
import {Field, Form, Formik} from "formik";
|
||||
import React, {useState} from "react";
|
||||
import colors from "qqq/assets/theme/base/colors";
|
||||
import MDInput from "qqq/components/legacy/MDInput";
|
||||
import ValueUtils from "qqq/utils/qqq/ValueUtils";
|
||||
|
||||
|
||||
export interface DropdownOption
|
||||
{
|
||||
id: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
/////////////////////////
|
||||
// inputs and defaults //
|
||||
/////////////////////////
|
||||
interface Props
|
||||
{
|
||||
name: string;
|
||||
defaultValue?: any;
|
||||
label?: string;
|
||||
startIcon?: string;
|
||||
width?: number;
|
||||
disableClearable?: boolean;
|
||||
allowBackAndForth?: boolean;
|
||||
backAndForthInverted?: boolean;
|
||||
dropdownOptions?: DropdownOption[];
|
||||
onChangeCallback?: (dropdownLabel: string, data: any) => void;
|
||||
sx?: SxProps<Theme>;
|
||||
}
|
||||
|
||||
interface StartAndEndDate
|
||||
{
|
||||
startDate?: string,
|
||||
endDate?: string
|
||||
}
|
||||
|
||||
function parseCustomTimeValuesFromDefaultValue(defaultValue: any): StartAndEndDate
|
||||
{
|
||||
const customTimeValues: StartAndEndDate = {};
|
||||
if (defaultValue && defaultValue.id)
|
||||
{
|
||||
var parts = defaultValue.id.split(",");
|
||||
if (parts.length >= 2)
|
||||
{
|
||||
customTimeValues["startDate"] = ValueUtils.formatDateTimeValueForForm(parts[1]);
|
||||
}
|
||||
if (parts.length >= 3)
|
||||
{
|
||||
customTimeValues["endDate"] = ValueUtils.formatDateTimeValueForForm(parts[2]);
|
||||
}
|
||||
}
|
||||
|
||||
return (customTimeValues);
|
||||
}
|
||||
|
||||
function makeBackendValuesFromFrontendValues(frontendDefaultValues: StartAndEndDate): StartAndEndDate
|
||||
{
|
||||
const backendTimeValues: StartAndEndDate = {};
|
||||
if (frontendDefaultValues && frontendDefaultValues.startDate)
|
||||
{
|
||||
backendTimeValues.startDate = ValueUtils.frontendLocalZoneDateTimeStringToUTCStringForBackend(frontendDefaultValues.startDate);
|
||||
}
|
||||
if (frontendDefaultValues && frontendDefaultValues.endDate)
|
||||
{
|
||||
backendTimeValues.endDate = ValueUtils.frontendLocalZoneDateTimeStringToUTCStringForBackend(frontendDefaultValues.endDate);
|
||||
}
|
||||
return (backendTimeValues);
|
||||
}
|
||||
|
||||
function WidgetDropdownMenu({name, defaultValue, label, startIcon, width, disableClearable, allowBackAndForth, backAndForthInverted, dropdownOptions, onChangeCallback, sx}: Props): JSX.Element
|
||||
{
|
||||
const [customTimesVisible, setCustomTimesVisible] = useState(defaultValue && defaultValue.id && defaultValue.id.startsWith("custom,"));
|
||||
const [customTimeValuesFrontend, setCustomTimeValuesFrontend] = useState(parseCustomTimeValuesFromDefaultValue(defaultValue) as StartAndEndDate);
|
||||
const [customTimeValuesBackend, setCustomTimeValuesBackend] = useState(makeBackendValuesFromFrontendValues(customTimeValuesFrontend) as StartAndEndDate);
|
||||
const [debounceTimeout, setDebounceTimeout] = useState(null as any);
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [value, setValue] = useState(defaultValue);
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
|
||||
const [backDisabled, setBackDisabled] = useState(false);
|
||||
const [forthDisabled, setForthDisabled] = useState(false);
|
||||
|
||||
const doForceOpen = (event: React.MouseEvent<HTMLDivElement>) =>
|
||||
{
|
||||
setIsOpen(true);
|
||||
};
|
||||
|
||||
function getSelectedIndex(value: DropdownOption)
|
||||
{
|
||||
let currentIndex = null;
|
||||
for (let i = 0; i < dropdownOptions.length; i++)
|
||||
{
|
||||
if (value && dropdownOptions[i].id == value.id)
|
||||
{
|
||||
currentIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return currentIndex;
|
||||
}
|
||||
|
||||
const navigateBackAndForth = (event: React.MouseEvent, direction: -1 | 1) =>
|
||||
{
|
||||
event.stopPropagation();
|
||||
let currentIndex = getSelectedIndex(value);
|
||||
|
||||
if (currentIndex == null)
|
||||
{
|
||||
console.log("No current value.... TODO");
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentIndex == 0 && direction == -1)
|
||||
{
|
||||
console.log("Can't go -1");
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentIndex == dropdownOptions.length - 1 && direction == 1)
|
||||
{
|
||||
console.log("Can't go +1");
|
||||
return;
|
||||
}
|
||||
|
||||
handleOnChange(event, dropdownOptions[currentIndex + direction], "navigatedBackAndForth");
|
||||
};
|
||||
|
||||
|
||||
const handleOnChange = (event: any, newValue: any, reason: string) =>
|
||||
{
|
||||
setValue(newValue);
|
||||
|
||||
const isTimeframeCustom = name == "timeframe" && newValue && newValue.id == "custom";
|
||||
setCustomTimesVisible(isTimeframeCustom);
|
||||
|
||||
if (isTimeframeCustom)
|
||||
{
|
||||
callOnChangeCallbackIfCustomTimeframeHasDateValues();
|
||||
}
|
||||
else
|
||||
{
|
||||
onChangeCallback(label, newValue);
|
||||
}
|
||||
|
||||
let currentIndex = getSelectedIndex(value);
|
||||
if(currentIndex == 0)
|
||||
{
|
||||
backAndForthInverted ? setForthDisabled(true) : setBackDisabled(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
backAndForthInverted ? setForthDisabled(false) : setBackDisabled(false);
|
||||
}
|
||||
|
||||
if (currentIndex == dropdownOptions.length - 1)
|
||||
{
|
||||
backAndForthInverted ? setBackDisabled(true) : setForthDisabled(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
backAndForthInverted ? setBackDisabled(false) : setForthDisabled(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOnInputChange = (event: any, newValue: any, reason: string) =>
|
||||
{
|
||||
setInputValue(newValue);
|
||||
};
|
||||
|
||||
const callOnChangeCallbackIfCustomTimeframeHasDateValues = () =>
|
||||
{
|
||||
if (customTimeValuesBackend["startDate"] && customTimeValuesBackend["endDate"])
|
||||
{
|
||||
onChangeCallback(label, {id: `custom,${customTimeValuesBackend["startDate"]},${customTimeValuesBackend["endDate"]}`, label: "Custom"});
|
||||
}
|
||||
};
|
||||
|
||||
let customTimes = <></>;
|
||||
if (name == "timeframe")
|
||||
{
|
||||
const handleSubmit = async (values: any, actions: any) =>
|
||||
{
|
||||
};
|
||||
|
||||
const dateChanged = (fieldName: "startDate" | "endDate", event: any) =>
|
||||
{
|
||||
customTimeValuesFrontend[fieldName] = event.target.value;
|
||||
customTimeValuesBackend[fieldName] = ValueUtils.frontendLocalZoneDateTimeStringToUTCStringForBackend(event.target.value);
|
||||
|
||||
clearTimeout(debounceTimeout);
|
||||
const newDebounceTimeout = setTimeout(() =>
|
||||
{
|
||||
callOnChangeCallbackIfCustomTimeframeHasDateValues();
|
||||
}, 500);
|
||||
setDebounceTimeout(newDebounceTimeout);
|
||||
};
|
||||
|
||||
customTimes = <Box sx={{display: "inline-block", position: "relative", top: "-7px"}}>
|
||||
<Collapse orientation="horizontal" in={customTimesVisible}>
|
||||
<Formik initialValues={customTimeValuesFrontend} onSubmit={handleSubmit}>
|
||||
{({}) => (
|
||||
<Form id="timeframe-form" autoComplete="off">
|
||||
<Field name="startDate" type="datetime-local" as={MDInput} variant="standard" label="Custom Timeframe Start" InputLabelProps={{shrink: true}} InputProps={{size: "small"}} sx={{ml: 2, width: 198}} onChange={(event: any) => dateChanged("startDate", event)} />
|
||||
<Field name="endDate" type="datetime-local" as={MDInput} variant="standard" label="Custom Timeframe End" InputLabelProps={{shrink: true}} InputProps={{size: "small"}} sx={{ml: 2, width: 198}} onChange={(event: any) => dateChanged("endDate", event)} />
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</Collapse>
|
||||
</Box>;
|
||||
}
|
||||
|
||||
const startAdornment = startIcon ? <Icon sx={{fontSize: "1.25rem!important", color: colors.gray.main, paddingLeft: allowBackAndForth ? "auto" : "0.25rem", width: allowBackAndForth ? "1.5rem" : "1.75rem"}}>{startIcon}</Icon> : null;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// we tried this end-adornment, for a different style of down-arrow - but by using it, we then messed something else up (i forget what), so... not used right now //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
const endAdornment = <InputAdornment position="end" sx={{position: "absolute", right: allowBackAndForth ? "-0.5rem" : "0.5rem"}}><Icon sx={{fontSize: "1.75rem!important", color: colors.gray.main}}>keyboard_arrow_down</Icon></InputAdornment>;
|
||||
|
||||
const fontSize = "1rem";
|
||||
let optionPaddingLeftRems = 0.75;
|
||||
if(startIcon)
|
||||
{
|
||||
optionPaddingLeftRems += allowBackAndForth ? 1.5 : 1.75
|
||||
}
|
||||
if(allowBackAndForth)
|
||||
{
|
||||
optionPaddingLeftRems += 2.5;
|
||||
}
|
||||
|
||||
return (
|
||||
dropdownOptions ? (
|
||||
<Box sx={{whiteSpace: "nowrap", display: "flex",
|
||||
"& .MuiPopperUnstyled-root": {
|
||||
border: `1px solid ${colors.grayLines.main}`,
|
||||
borderTop: "none",
|
||||
borderRadius: "0 0 0.75rem 0.75rem",
|
||||
padding: 0,
|
||||
}, "& .MuiPaper-rounded": {
|
||||
borderRadius: "0 0 0.75rem 0.75rem",
|
||||
}
|
||||
}} className="dashboardDropdownMenu">
|
||||
<Autocomplete
|
||||
id={`${label}-combo-box`}
|
||||
|
||||
defaultValue={defaultValue}
|
||||
value={value}
|
||||
onChange={handleOnChange}
|
||||
inputValue={inputValue}
|
||||
onInputChange={handleOnInputChange}
|
||||
|
||||
isOptionEqualToValue={(option, value) => option.id === value.id}
|
||||
|
||||
open={isOpen}
|
||||
onOpen={() => setIsOpen(true)}
|
||||
onClose={() => setIsOpen(false)}
|
||||
|
||||
size="small"
|
||||
disablePortal
|
||||
disableClearable={disableClearable}
|
||||
options={dropdownOptions}
|
||||
sx={{
|
||||
...sx,
|
||||
cursor: "pointer",
|
||||
display: "inline-block",
|
||||
"& .MuiOutlinedInput-notchedOutline": {
|
||||
border: "none"
|
||||
},
|
||||
}}
|
||||
renderInput={(params: any) =>
|
||||
<>
|
||||
<Box sx={{width: `${width}px`, background: "white", borderRadius: isOpen ? "0.75rem 0.75rem 0 0" : "0.75rem", border: `1px solid ${colors.grayLines.main}`, "& *": {cursor: "pointer"}}} display="flex" alignItems="center" onClick={(event) => doForceOpen(event)}>
|
||||
{allowBackAndForth && <IconButton onClick={(event) => navigateBackAndForth(event, backAndForthInverted ? 1 : -1)} disabled={backDisabled}><Icon>navigate_before</Icon></IconButton>}
|
||||
<TextField {...params} placeholder={label} sx={{
|
||||
"& .MuiInputBase-input": {
|
||||
fontSize: fontSize
|
||||
}
|
||||
}} InputProps={{...params.InputProps, startAdornment: startAdornment/*, endAdornment: endAdornment*/}}
|
||||
/>
|
||||
{allowBackAndForth && <IconButton onClick={(event) => navigateBackAndForth(event, backAndForthInverted ? -1 : 1)} disabled={forthDisabled}><Icon>navigate_next</Icon></IconButton>}
|
||||
</Box>
|
||||
</>
|
||||
}
|
||||
renderOption={(props, option: DropdownOption) => (
|
||||
<li {...props} style={{whiteSpace: "normal", fontSize: fontSize, paddingLeft: `${optionPaddingLeftRems}rem`}}>{option.label}</li>
|
||||
)}
|
||||
|
||||
noOptionsText={<Box fontSize={fontSize}>No options found</Box>}
|
||||
|
||||
slotProps={{
|
||||
popper: {
|
||||
sx: {
|
||||
width: `${width}px!important`
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{customTimes}
|
||||
</Box>
|
||||
) : null
|
||||
);
|
||||
}
|
||||
|
||||
export default WidgetDropdownMenu;
|
Reference in New Issue
Block a user