123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758 |
- /*
- * 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<HTMLDivElement>(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<DragItem, void, { handlerId: string }>(
- () => ({
- accept: dropTypes || "",
- hover: (item: DragItem) => {
- item.index = index;
- item.targetId = targetId;
- },
- }),
- [dropTypes, index, targetId]
- );
- drag(drop(itemRef));
- return (
- <ListItemButton
- onClick={clickHandler}
- data-id={value}
- dense
- disabled={disabled}
- ref={itemRef}
- sx={isDragging ? dragSx : undefined}
- >
- <ListItemIcon>
- <Checkbox
- disabled={disabled}
- edge="start"
- checked={selectedValue.includes(value)}
- tabIndex={-1}
- disableRipple
- />
- </ListItemIcon>
- {typeof item === "string" ? (
- <ListItemText primary={item} />
- ) : (
- <ListItemAvatar>
- <LovImage item={item} />
- </ListItemAvatar>
- )}
- </ListItemButton>
- );
- };
- 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<HTMLLIElement>, option: LovItem) => (
- <li {...props} key={option.id}>
- {typeof option.item === "string" ? option.item : <LovImage item={option.item} />}
- </li>
- );
- 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<string[]>([]);
- 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<HTMLElement>) => {
- if (active) {
- const { id: key = "" } = evt.currentTarget.dataset;
- selectHandler(key);
- }
- },
- [active, selectHandler]
- );
- const changeHandler = useCallback(
- (evt: ChangeEvent<HTMLInputElement>) => {
- if (active) {
- const { id: key = "" } = (evt.currentTarget as HTMLElement).parentElement?.dataset || {};
- selectHandler(key);
- }
- },
- [active, selectHandler]
- );
- const handleChange = useCallback(
- (event: SelectChangeEvent<typeof selectedValue>) => {
- 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<HTMLInputElement>, 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<LovItem | LovItem[] | null>(() => (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<HTMLInputElement>) => setSearchValue(e.target.value), []);
- const dropdownValue = ((dropdown || isRadio) &&
- (multiple ? selectedValue : selectedValue.length ? selectedValue[0] : "")) as string[];
- return (
- <>
- {isRadio || isCheck ? (
- <FormControl sx={controlSx} className={`${className} ${getComponentClassName(props.children)}`}>
- {props.label ? <FormLabel>{props.label}</FormLabel> : null}
- <Tooltip title={hover || ""}>
- {isRadio ? (
- <RadioGroup
- value={dropdownValue}
- onChange={handleChange}
- className={getSuffixedClassNames(className, "-radio-group")}
- sx={heightSx}
- >
- {lovList.map((item) => (
- <FormControlLabel
- key={item.id}
- value={item.id}
- control={<Radio />}
- label={
- typeof item.item === "string" ? (
- item.item
- ) : (
- <LovImage item={item.item as Icon} />
- )
- }
- style={getStyles(item.id, selectedValue, theme)}
- disabled={!active}
- />
- ))}
- </RadioGroup>
- ) : (
- <FormGroup className={getSuffixedClassNames(className, "-check-group")} sx={heightSx}>
- {lovList.map((item) => (
- <FormControlLabel
- key={item.id}
- control={
- <Checkbox
- data-id={item.id}
- checked={selectedValue.includes(item.id)}
- onChange={changeHandler}
- />
- }
- label={
- typeof item.item === "string" ? (
- item.item
- ) : (
- <LovImage item={item.item as Icon} />
- )
- }
- style={getStyles(item.id, selectedValue, theme)}
- disabled={!active}
- ></FormControlLabel>
- ))}
- </FormGroup>
- )}
- </Tooltip>
- </FormControl>
- ) : dropdown ? (
- filter ? (
- <Tooltip title={hover || ""} placement="top">
- <Autocomplete
- id={id}
- disabled={!active}
- multiple={multiple}
- options={lovList}
- value={autoValue}
- onChange={handleAutoChange}
- getOptionLabel={getOptionLabel}
- getOptionKey={getOptionKey}
- isOptionEqualToValue={isOptionEqualToValue}
- sx={controlSx}
- className={`${className} ${getComponentClassName(props.children)}`}
- renderInput={(params) => <TextField {...params} label={props.label} margin="dense" />}
- renderOption={renderOption}
- />
- </Tooltip>
- ) : (
- <FormControl sx={controlSx} className={`${className} ${getComponentClassName(props.children)}`}>
- {props.label ? <InputLabel disableAnimation>{props.label}</InputLabel> : null}
- <Tooltip title={hover || ""} placement="top">
- <Select
- id={id}
- multiple={multiple}
- value={dropdownValue}
- onChange={handleChange}
- input={
- <OutlinedInput
- label={props.label}
- startAdornment={
- multiple && showSelectAll ? (
- <Tooltip
- title={
- selectedValue.length == lovList.length
- ? "Deselect All"
- : "Select All"
- }
- >
- <Checkbox
- disabled={!active}
- indeterminate={
- selectedValue.length > 0 &&
- selectedValue.length < lovList.length
- }
- checked={selectedValue.length == lovList.length}
- onChange={handleCheckAllChange}
- ></Checkbox>
- </Tooltip>
- ) : null
- }
- />
- }
- disabled={!active}
- renderValue={(selected) => (
- <Box sx={renderBoxSx}>
- {typeof selectionMessage === "string"
- ? selectionMessage
- : lovList
- .filter((it) =>
- Array.isArray(selected)
- ? selected.includes(it.id)
- : selected === it.id
- )
- .map((item, idx) => {
- if (multiple) {
- const chipProps = {} as Record<string, unknown>;
- if (typeof item.item === "string") {
- chipProps.label = item.item;
- } else {
- chipProps.label = item.item.text || "";
- chipProps.avatar = <Avatar src={item.item.path} />;
- }
- return (
- <Chip
- key={item.id}
- {...chipProps}
- onDelete={handleDelete}
- data-id={item.id}
- onMouseDown={doNotPropagateEvent}
- disabled={!active}
- />
- );
- } else if (idx === 0) {
- return typeof item.item === "string" ? (
- item.item
- ) : (
- <LovImage item={item.item} />
- );
- } else {
- return null;
- }
- })}
- </Box>
- )}
- MenuProps={getMenuProps(height)}
- >
- {lovList.map((item) => (
- <MenuItem
- key={item.id}
- value={item.id}
- style={getStyles(item.id, selectedValue, theme)}
- disabled={item.id === null}
- >
- {typeof item.item === "string" ? (
- item.item
- ) : (
- <LovImage item={item.item as Icon} />
- )}
- </MenuItem>
- ))}
- </Select>
- </Tooltip>
- </FormControl>
- )
- ) : (
- <FormControl sx={controlSx} className={`${className} ${getComponentClassName(props.children)}`}>
- {props.label ? (
- <InputLabel disableAnimation className="static-label">
- {props.label}
- </InputLabel>
- ) : null}
- <Tooltip title={hover || ""}>
- <Paper sx={paperSx}>
- {filter ? (
- <Box>
- <OutlinedInput
- margin="dense"
- placeholder="Search field"
- value={searchValue}
- onChange={handleInput}
- disabled={!active}
- startAdornment={
- multiple && showSelectAll ? (
- <Tooltip
- title={
- selectedValue.length == lovList.length
- ? "Deselect All"
- : "Select All"
- }
- >
- <Checkbox
- disabled={!active}
- indeterminate={
- selectedValue.length > 0 &&
- selectedValue.length < lovList.length
- }
- checked={selectedValue.length == lovList.length}
- onChange={handleCheckAllChange}
- ></Checkbox>
- </Tooltip>
- ) : null
- }
- />
- </Box>
- ) : multiple && showSelectAll ? (
- <Box paddingLeft={1}>
- <FormControlLabel
- control={
- <Checkbox
- disabled={!active}
- indeterminate={
- selectedValue.length > 0 && selectedValue.length < lovList.length
- }
- checked={selectedValue.length == lovList.length}
- onChange={handleCheckAllChange}
- ></Checkbox>
- }
- label={selectedValue.length == lovList.length ? "Deselect All" : "Select All"}
- />
- </Box>
- ) : null}
- <List sx={listSx} id={id} ref={dropRef}>
- {lovList.map((elt, idx) =>
- showItem(elt, searchValue) ? (
- multiple ? (
- <MultipleItem
- key={elt.id}
- value={elt.id}
- item={elt.item}
- selectedValue={selectedValue}
- clickHandler={clickHandler}
- disabled={!active}
- dragType={props.dragType}
- dropTypes={dropTypes}
- index={idx}
- handleDrop={handleDrop}
- lovVarName={lovVarName}
- targetId={id}
- />
- ) : (
- <SingleItem
- key={elt.id}
- value={elt.id}
- item={elt.item}
- selectedValue={selectedValue}
- clickHandler={clickHandler}
- disabled={!active}
- dragType={props.dragType}
- dropTypes={dropTypes}
- index={idx}
- handleDrop={handleDrop}
- lovVarName={lovVarName}
- targetId={id}
- />
- )
- ) : null
- )}
- </List>
- </Paper>
- </Tooltip>
- </FormControl>
- )}
- {props.children}
- </>
- );
- };
- export default Selector;
|