/* * Copyright 2021-2025 Avaiga Private Limited * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * specific language governing permissions and limitations under the License. */ import React, { useState, useCallback, useEffect, useMemo, useRef, CSSProperties, MouseEvent, ChangeEvent, SyntheticEvent, HTMLAttributes, } from "react"; import Autocomplete from "@mui/material/Autocomplete"; import Avatar from "@mui/material/Avatar"; import Box from "@mui/material/Box"; import Checkbox from "@mui/material/Checkbox"; import Chip from "@mui/material/Chip"; import FormControl from "@mui/material/FormControl"; import FormControlLabel from "@mui/material/FormControlLabel"; import FormGroup from "@mui/material/FormGroup"; import FormLabel from "@mui/material/FormLabel"; import InputLabel from "@mui/material/InputLabel"; import List from "@mui/material/List"; import ListItemButton from "@mui/material/ListItemButton"; import ListItemIcon from "@mui/material/ListItemIcon"; import ListItemText from "@mui/material/ListItemText"; import ListItemAvatar from "@mui/material/ListItemAvatar"; import MenuItem from "@mui/material/MenuItem"; import OutlinedInput from "@mui/material/OutlinedInput"; import Paper from "@mui/material/Paper"; import Tooltip from "@mui/material/Tooltip"; import Radio from "@mui/material/Radio"; import RadioGroup from "@mui/material/RadioGroup"; import Select, { SelectChangeEvent } from "@mui/material/Select"; import TextField from "@mui/material/TextField"; import { Theme, useTheme } from "@mui/material"; import { useDrag, useDrop } from "react-dnd"; import { doNotPropagateEvent, getSuffixedClassNames, getUpdateVar } from "./utils"; import { createSendActionNameAction, createSendUpdateAction } from "../../context/taipyReducers"; import { DragItem, dragSx, ItemProps, LovImage, paperBaseSx, SelTreeProps, showItem, SingleItem, useLovListMemo, } from "./lovUtils"; import { useClassNames, useDispatch, useDispatchRequestUpdateOnFirstRender, useDynamicProperty, useModule, } from "../../utils/hooks"; import { Icon } from "../../utils/icon"; import { LovItem } from "../../utils/lov"; import { getComponentClassName } from "./TaipyStyle"; const MultipleItem = ({ value, clickHandler, selectedValue, item, disabled, dragType = "", dropTypes, handleDrop, index = -1, lovVarName, targetId, }: ItemProps) => { const itemRef = useRef(null); const getDragItem = useCallback( () => (dragType && !disabled ? { id: value, index: -1 } : null), [dragType, disabled, value] ); const [{ isDragging }, drag] = useDrag( () => ({ type: dragType, item: getDragItem, collect: (monitor) => ({ isDragging: monitor.isDragging(), }), end: (item: DragItem, monitor) => { const dropResult = monitor.getDropResult(); if (dropResult) { handleDrop?.(item.id, item.index, lovVarName || "", item.targetId); } }, }), [dragType, getDragItem, lovVarName, targetId] ); const [, drop] = useDrop( () => ({ accept: dropTypes || "", hover: (item: DragItem) => { item.index = index; item.targetId = targetId; }, }), [dropTypes, index, targetId] ); drag(drop(itemRef)); return ( {typeof item === "string" ? ( ) : ( )} ); }; const ITEM_HEIGHT = 48; const ITEM_PADDING_TOP = 8; const getMenuProps = (height?: string | number) => ({ PaperProps: { style: { maxHeight: height || ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP, }, }, }); const getStyles = (id: string, ids: readonly string[], theme: Theme) => ({ fontWeight: ids.indexOf(id) === -1 ? theme.typography.fontWeightRegular : theme.typography.fontWeightMedium, }); const getOptionLabel = (option: LovItem) => (typeof option.item === "string" ? option.item : option.item?.text) || ""; const getOptionKey = (option: LovItem) => "" + option.id; const isOptionEqualToValue = (option: LovItem, value: LovItem) => option.id == value.id; const renderOption = (props: HTMLAttributes, option: LovItem) => (
  • {typeof option.item === "string" ? option.item : }
  • ); const getLovItemsFromStr = (value: string | string[] | undefined, lovList: LovItem[], multiple: boolean) => { const val = multiple ? Array.isArray(value) ? value : [value] : Array.isArray(value) && value.length ? value[0] : value; return Array.isArray(val) ? (val.map((v) => lovList.find((item) => item.id == "" + v)).filter((i) => i) as LovItem[]) : (val && lovList.find((item) => item.id == "" + val)) || null; }; const renderBoxSx = { display: "flex", flexWrap: "wrap", gap: 0.5, width: "100%", } as CSSProperties; interface SelectorProps extends SelTreeProps { dropdown?: boolean; mode?: string; defaultSelectionMessage?: string; selectionMessage?: string; showSelectAll?: boolean; } const Selector = (props: SelectorProps) => { const { id, defaultValue = "", value, updateVarName = "", defaultLov = "", filter = false, propagate = true, lov, updateVars = "", width = "100%", height, valueById, mode = "", showSelectAll = false, } = props; const [searchValue, setSearchValue] = useState(""); const [selectedValue, setSelectedValue] = useState([]); const dispatch = useDispatch(); const module = useModule(); const theme = useTheme(); const className = useClassNames(props.libClassName, props.dynamicClassName, props.className); const active = useDynamicProperty(props.active, props.defaultActive, true); const hover = useDynamicProperty(props.hoverText, props.defaultHoverText, undefined); const selectionMessage = useDynamicProperty(props.selectionMessage, props.defaultSelectionMessage, undefined); useDispatchRequestUpdateOnFirstRender(dispatch, id, module, updateVars, updateVarName); const isRadio = mode && mode.toLocaleLowerCase() == "radio"; const isCheck = mode && mode.toLocaleLowerCase() == "check"; const dropdown = isRadio || isCheck || props.dropdown === undefined ? false : props.dropdown; const multiple = isCheck ? true : isRadio || props.multiple === undefined ? false : props.multiple; const lovVarName = useMemo(() => getUpdateVar(updateVars, "lov"), [updateVars]); // Droppable area for drag and drop const dropTypes = useMemo(() => { if (props.dropTypes) { try { return JSON.parse(props.dropTypes); } catch (e) { console.error("Invalid dropTypes JSON string", e); } } return []; }, [props.dropTypes]); const [, dropRef] = useDrop( () => ({ accept: dropTypes, hover: (item: DragItem) => { item.index = -1; item.targetId = id; }, }), [dropTypes, id] ); const handleDrop = useCallback( (itemId: string, dropIndex: number, targetVarName: string, targetId?: string) => { dispatch( createSendActionNameAction(props.onAction, module, { reason: "drop", source_var: lovVarName, source_id: id, item_id: itemId, drop_index: dropIndex, target_var: targetVarName, target_id: targetId, }) ); }, [lovVarName, dispatch, module, props.onAction, id] ); const lovList = useLovListMemo(lov, defaultLov); const listSx = useMemo( () => ({ bgcolor: "transparent", overflowY: "auto", width: "100%", maxWidth: width, }), [width] ); const heightSx = useMemo(() => { if (!height) { return undefined; } return { maxHeight: height, display: "flex", flexFlow: "column nowrap", overflowY: "auto", }; }, [height]); const paperSx = useMemo(() => { let sx = paperBaseSx; if (height !== undefined) { sx = { ...sx, maxHeight: height }; } return sx; }, [height]); const controlSx = useMemo( () => ({ my: 1, mx: 0, width: width, "& .MuiFormControl-root": { maxWidth: "unset", my: 0, "& .MuiInputBase-root": { minHeight: 48, "& input": { minHeight: "unset" } }, }, }), [width] ); useEffect(() => { if (value !== undefined && value !== null) { setSelectedValue(Array.isArray(value) ? value.map((v) => "" + v) : ["" + value]); setAutoValue(getLovItemsFromStr(value, lovList, multiple)); } else if (defaultValue) { let parsedValue; try { parsedValue = JSON.parse(defaultValue); } catch { parsedValue = defaultValue; } setSelectedValue(Array.isArray(parsedValue) ? parsedValue : [parsedValue]); setAutoValue(getLovItemsFromStr(parsedValue, lovList, multiple)); } }, [defaultValue, value, lovList, multiple]); const selectHandler = useCallback( (key: string) => { setSelectedValue((keys) => { if (multiple) { const newKeys = [...keys]; const p = newKeys.indexOf(key); if (p === -1) { newKeys.push(key); } else { newKeys.splice(p, 1); } dispatch( createSendUpdateAction( updateVarName, newKeys, module, props.onChange, propagate, valueById ? undefined : lovVarName ) ); return newKeys; } else { dispatch( createSendUpdateAction( updateVarName, key, module, props.onChange, propagate, valueById ? undefined : lovVarName ) ); return [key]; } }); }, [updateVarName, dispatch, multiple, propagate, lovVarName, valueById, props.onChange, module] ); const clickHandler = useCallback( (evt: MouseEvent) => { if (active) { const { id: key = "" } = evt.currentTarget.dataset; selectHandler(key); } }, [active, selectHandler] ); const changeHandler = useCallback( (evt: ChangeEvent) => { if (active) { const { id: key = "" } = (evt.currentTarget as HTMLElement).parentElement?.dataset || {}; selectHandler(key); } }, [active, selectHandler] ); const handleChange = useCallback( (event: SelectChangeEvent) => { const { target: { value }, } = event; setSelectedValue(Array.isArray(value) ? value : [value]); dispatch( createSendUpdateAction( updateVarName, value, module, props.onChange, propagate, valueById ? undefined : lovVarName ) ); }, [dispatch, updateVarName, propagate, lovVarName, valueById, props.onChange, module] ); const handleCheckAllChange = useCallback( (event: SelectChangeEvent, checked: boolean) => { const sel = checked ? lovList.map((elt) => elt.id) : []; setSelectedValue(sel); dispatch( createSendUpdateAction( updateVarName, sel, module, props.onChange, propagate, valueById ? undefined : lovVarName ) ); }, [lovList, dispatch, updateVarName, propagate, lovVarName, valueById, props.onChange, module] ); const [autoValue, setAutoValue] = useState(() => (multiple ? [] : null)); const handleAutoChange = useCallback( (e: SyntheticEvent, sel: LovItem | LovItem[] | null) => { setAutoValue(sel); setSelectedValue(Array.isArray(sel) ? sel.map((item) => item.id) : sel ? [sel.id] : []); dispatch( createSendUpdateAction( updateVarName, Array.isArray(sel) ? sel.map((item) => item.id) : sel?.id, module, props.onChange, propagate, valueById ? undefined : lovVarName ) ); }, [dispatch, updateVarName, propagate, lovVarName, valueById, props.onChange, module] ); const handleDelete = useCallback( (e: React.SyntheticEvent) => { const id = e.currentTarget?.parentElement?.getAttribute("data-id"); id && setSelectedValue((oldKeys) => { const keys = oldKeys.filter((valId) => valId !== id); dispatch( createSendUpdateAction( updateVarName, keys, module, props.onChange, propagate, valueById ? undefined : lovVarName ) ); return keys; }); }, [updateVarName, propagate, dispatch, lovVarName, valueById, props.onChange, module] ); const handleInput = useCallback((e: React.ChangeEvent) => setSearchValue(e.target.value), []); const dropdownValue = ((dropdown || isRadio) && (multiple ? selectedValue : selectedValue.length ? selectedValue[0] : "")) as string[]; return ( <> {isRadio || isCheck ? ( {props.label ? {props.label} : null} {isRadio ? ( {lovList.map((item) => ( } label={ typeof item.item === "string" ? ( item.item ) : ( ) } style={getStyles(item.id, selectedValue, theme)} disabled={!active} /> ))} ) : ( {lovList.map((item) => ( } label={ typeof item.item === "string" ? ( item.item ) : ( ) } style={getStyles(item.id, selectedValue, theme)} disabled={!active} > ))} )} ) : dropdown ? ( filter ? ( } renderOption={renderOption} /> ) : ( {props.label ? {props.label} : null} ) ) : ( {props.label ? ( {props.label} ) : null} {filter ? ( 0 && selectedValue.length < lovList.length } checked={selectedValue.length == lovList.length} onChange={handleCheckAllChange} > ) : null } /> ) : multiple && showSelectAll ? ( 0 && selectedValue.length < lovList.length } checked={selectedValue.length == lovList.length} onChange={handleCheckAllChange} > } label={selectedValue.length == lovList.length ? "Deselect All" : "Select All"} /> ) : null} {lovList.map((elt, idx) => showItem(elt, searchValue) ? ( multiple ? ( ) : ( ) ) : null )} )} {props.children} ); }; export default Selector;