diff --git a/src/qqq/components/widgets/Widget.tsx b/src/qqq/components/widgets/Widget.tsx
index 4894211..59f5258 100644
--- a/src/qqq/components/widgets/Widget.tsx
+++ b/src/qqq/components/widgets/Widget.tsx
@@ -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 (
)
+ return (
);
}
else
{
return ({this.iconName});
}
- }
+ };
}
@@ -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 (
-
);
@@ -332,7 +402,12 @@ function Widget(props: React.PropsWithChildren): 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): 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 = (
{labelToUse}
);
- if(props.widgetMetaData.tooltip)
+ if (props.widgetMetaData.tooltip)
{
- labelElement = {labelElement}
+ labelElement = {labelElement};
}
const errorLoading = props.widgetData?.errorLoading !== undefined && props.widgetData?.errorLoading === true;
@@ -578,7 +653,7 @@ function Widget(props: React.PropsWithChildren): JSX.Element
}
;
- const padding = props.omitPadding? "auto" : "24px 16px";
+ const padding = props.omitPadding ? "auto" : "24px 16px";
return props.widgetMetaData?.isCard
?
{widgetContent}
diff --git a/src/qqq/components/widgets/components/DropdownMenu.tsx b/src/qqq/components/widgets/components/DropdownMenu.tsx
deleted file mode 100644
index 7266cb7..0000000
--- a/src/qqq/components/widgets/components/DropdownMenu.tsx
+++ /dev/null
@@ -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 .
- */
-
-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;
-}
-
-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 =
-
-
- {({}) => (
-
- )}
-
-
- ;
- }
-
- return (
- dropdownOptions ? (
-
- option.id === value.id}
- renderInput={(params: any) => }
- renderOption={(props, option: DropdownOption) => (
- {option.label}
- )}
- />
- {customTimes}
-
- ) : null
- );
-}
-
-export default DropdownMenu;
diff --git a/src/qqq/components/widgets/components/WidgetDropdownMenu.tsx b/src/qqq/components/widgets/components/WidgetDropdownMenu.tsx
new file mode 100644
index 0000000..743b0a9
--- /dev/null
+++ b/src/qqq/components/widgets/components/WidgetDropdownMenu.tsx
@@ -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 .
+ */
+
+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;
+}
+
+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) =>
+ {
+ 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 =
+
+
+ {({}) => (
+
+ )}
+
+
+ ;
+ }
+
+ const startAdornment = startIcon ? {startIcon} : 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 = keyboard_arrow_down;
+
+ const fontSize = "1rem";
+ let optionPaddingLeftRems = 0.75;
+ if(startIcon)
+ {
+ optionPaddingLeftRems += allowBackAndForth ? 1.5 : 1.75
+ }
+ if(allowBackAndForth)
+ {
+ optionPaddingLeftRems += 2.5;
+ }
+
+ return (
+ dropdownOptions ? (
+
+ 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) =>
+ <>
+ doForceOpen(event)}>
+ {allowBackAndForth && navigateBackAndForth(event, backAndForthInverted ? 1 : -1)} disabled={backDisabled}>navigate_before}
+
+ {allowBackAndForth && navigateBackAndForth(event, backAndForthInverted ? -1 : 1)} disabled={forthDisabled}>navigate_next}
+
+ >
+ }
+ renderOption={(props, option: DropdownOption) => (
+ {option.label}
+ )}
+
+ noOptionsText={No options found}
+
+ slotProps={{
+ popper: {
+ sx: {
+ width: `${width}px!important`
+ }
+ }
+ }}
+ />
+ {customTimes}
+
+ ) : null
+ );
+}
+
+export default WidgetDropdownMenu;