Selector.tsx 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502
  1. /*
  2. * Copyright 2021-2024 Avaiga Private Limited
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
  5. * the License. You may obtain a copy of the License at
  6. *
  7. * http://www.apache.org/licenses/LICENSE-2.0
  8. *
  9. * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
  10. * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
  11. * specific language governing permissions and limitations under the License.
  12. */
  13. import React, {
  14. useState,
  15. useCallback,
  16. useEffect,
  17. useMemo,
  18. CSSProperties,
  19. MouseEvent,
  20. ChangeEvent,
  21. SyntheticEvent,
  22. HTMLAttributes,
  23. } from "react";
  24. import Autocomplete from "@mui/material/Autocomplete";
  25. import Avatar from "@mui/material/Avatar";
  26. import Box from "@mui/material/Box";
  27. import Checkbox from "@mui/material/Checkbox";
  28. import Chip from "@mui/material/Chip";
  29. import FormControl from "@mui/material/FormControl";
  30. import FormControlLabel from "@mui/material/FormControlLabel";
  31. import FormGroup from "@mui/material/FormGroup";
  32. import FormLabel from "@mui/material/FormLabel";
  33. import InputLabel from "@mui/material/InputLabel";
  34. import List from "@mui/material/List";
  35. import ListItemButton from "@mui/material/ListItemButton";
  36. import ListItemIcon from "@mui/material/ListItemIcon";
  37. import ListItemText from "@mui/material/ListItemText";
  38. import ListItemAvatar from "@mui/material/ListItemAvatar";
  39. import MenuItem from "@mui/material/MenuItem";
  40. import OutlinedInput from "@mui/material/OutlinedInput";
  41. import Paper from "@mui/material/Paper";
  42. import Tooltip from "@mui/material/Tooltip";
  43. import Radio from "@mui/material/Radio";
  44. import RadioGroup from "@mui/material/RadioGroup";
  45. import Select, { SelectChangeEvent } from "@mui/material/Select";
  46. import TextField from "@mui/material/TextField";
  47. import { Theme, useTheme } from "@mui/material";
  48. import { doNotPropagateEvent, getSuffixedClassNames, getUpdateVar } from "./utils";
  49. import { createSendUpdateAction } from "../../context/taipyReducers";
  50. import { ItemProps, LovImage, paperBaseSx, SelTreeProps, showItem, SingleItem, useLovListMemo } from "./lovUtils";
  51. import {
  52. useClassNames,
  53. useDispatch,
  54. useDispatchRequestUpdateOnFirstRender,
  55. useDynamicProperty,
  56. useModule,
  57. } from "../../utils/hooks";
  58. import { Icon } from "../../utils/icon";
  59. import { LovItem } from "../../utils/lov";
  60. const MultipleItem = ({ value, clickHandler, selectedValue, item, disabled }: ItemProps) => (
  61. <ListItemButton onClick={clickHandler} data-id={value} dense disabled={disabled}>
  62. <ListItemIcon>
  63. <Checkbox
  64. disabled={disabled}
  65. edge="start"
  66. checked={selectedValue.includes(value)}
  67. tabIndex={-1}
  68. disableRipple
  69. />
  70. </ListItemIcon>
  71. {typeof item === "string" ? (
  72. <ListItemText primary={item} />
  73. ) : (
  74. <ListItemAvatar>
  75. <LovImage item={item} />
  76. </ListItemAvatar>
  77. )}
  78. </ListItemButton>
  79. );
  80. const ITEM_HEIGHT = 48;
  81. const ITEM_PADDING_TOP = 8;
  82. const getMenuProps = (height?: string | number) => ({
  83. PaperProps: {
  84. style: {
  85. maxHeight: height || ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
  86. },
  87. },
  88. });
  89. const getStyles = (id: string, ids: readonly string[], theme: Theme) => ({
  90. fontWeight: ids.indexOf(id) === -1 ? theme.typography.fontWeightRegular : theme.typography.fontWeightMedium,
  91. });
  92. const getOptionLabel = (option: LovItem) => (typeof option.item === "string" ? option.item : option.item?.text) || "";
  93. const getOptionKey = (option: LovItem) => "" + option.id;
  94. const isOptionEqualToValue = (option: LovItem, value: LovItem) => option.id == value.id;
  95. const renderOption = (props: HTMLAttributes<HTMLLIElement>, option: LovItem) => (
  96. <li {...props}>{typeof option.item === "string" ? option.item : <LovImage item={option.item} />}</li>
  97. );
  98. const getLovItemsFromStr = (value: string | string[] | undefined, lovList: LovItem[], multiple: boolean) => {
  99. const val = multiple
  100. ? Array.isArray(value)
  101. ? value
  102. : [value]
  103. : Array.isArray(value) && value.length
  104. ? value[0]
  105. : value;
  106. return Array.isArray(val)
  107. ? (val.map((v) => lovList.find((item) => item.id == "" + v)).filter((i) => i) as LovItem[])
  108. : (val && lovList.find((item) => item.id == "" + val)) || null;
  109. };
  110. const renderBoxSx = {
  111. display: "flex",
  112. flexWrap: "wrap",
  113. gap: 0.5,
  114. width: "100%",
  115. } as CSSProperties;
  116. const Selector = (props: SelTreeProps) => {
  117. const {
  118. id,
  119. defaultValue = "",
  120. value,
  121. updateVarName = "",
  122. defaultLov = "",
  123. filter = false,
  124. propagate = true,
  125. lov,
  126. updateVars = "",
  127. width = "100%",
  128. height,
  129. valueById,
  130. mode = "",
  131. } = props;
  132. const [searchValue, setSearchValue] = useState("");
  133. const [selectedValue, setSelectedValue] = useState<string[]>([]);
  134. const dispatch = useDispatch();
  135. const module = useModule();
  136. const theme = useTheme();
  137. const className = useClassNames(props.libClassName, props.dynamicClassName, props.className);
  138. const active = useDynamicProperty(props.active, props.defaultActive, true);
  139. const hover = useDynamicProperty(props.hoverText, props.defaultHoverText, undefined);
  140. useDispatchRequestUpdateOnFirstRender(dispatch, id, module, updateVars, updateVarName);
  141. const isRadio = mode && mode.toLocaleLowerCase() == "radio";
  142. const isCheck = mode && mode.toLocaleLowerCase() == "check";
  143. const dropdown = isRadio || isCheck || props.dropdown === undefined ? false : props.dropdown;
  144. const multiple = isCheck ? true : isRadio || props.multiple === undefined ? false : props.multiple;
  145. const lovList = useLovListMemo(lov, defaultLov);
  146. const listSx = useMemo(
  147. () => ({
  148. bgcolor: "transparent",
  149. overflowY: "auto",
  150. width: "100%",
  151. maxWidth: width,
  152. }),
  153. [width]
  154. );
  155. const paperSx = useMemo(() => {
  156. let sx = paperBaseSx;
  157. if (height !== undefined) {
  158. sx = { ...sx, maxHeight: height };
  159. }
  160. return sx;
  161. }, [height]);
  162. const controlSx = useMemo(
  163. () => ({ my: 1, mx: 0, maxWidth: width, display: "flex", "& .MuiFormControl-root": { maxWidth: "unset" } }),
  164. [width]
  165. );
  166. useEffect(() => {
  167. if (value !== undefined && value !== null) {
  168. setSelectedValue(Array.isArray(value) ? value.map((v) => "" + v) : ["" + value]);
  169. setAutoValue(getLovItemsFromStr(value, lovList, multiple));
  170. } else if (defaultValue) {
  171. let parsedValue;
  172. try {
  173. parsedValue = JSON.parse(defaultValue);
  174. } catch (e) {
  175. parsedValue = defaultValue;
  176. }
  177. setSelectedValue(Array.isArray(parsedValue) ? parsedValue : [parsedValue]);
  178. setAutoValue(getLovItemsFromStr(parsedValue, lovList, multiple));
  179. }
  180. }, [defaultValue, value, lovList, multiple]);
  181. const selectHandler = useCallback(
  182. (key: string) => {
  183. setSelectedValue((keys) => {
  184. if (multiple) {
  185. const newKeys = [...keys];
  186. const p = newKeys.indexOf(key);
  187. if (p === -1) {
  188. newKeys.push(key);
  189. } else {
  190. newKeys.splice(p, 1);
  191. }
  192. dispatch(
  193. createSendUpdateAction(
  194. updateVarName,
  195. newKeys,
  196. module,
  197. props.onChange,
  198. propagate,
  199. valueById ? undefined : getUpdateVar(updateVars, "lov")
  200. )
  201. );
  202. return newKeys;
  203. } else {
  204. dispatch(
  205. createSendUpdateAction(
  206. updateVarName,
  207. key,
  208. module,
  209. props.onChange,
  210. propagate,
  211. valueById ? undefined : getUpdateVar(updateVars, "lov")
  212. )
  213. );
  214. return [key];
  215. }
  216. });
  217. },
  218. [updateVarName, dispatch, multiple, propagate, updateVars, valueById, props.onChange, module]
  219. );
  220. const clickHandler = useCallback(
  221. (evt: MouseEvent<HTMLElement>) => {
  222. if (active) {
  223. const { id: key = "" } = evt.currentTarget.dataset;
  224. selectHandler(key);
  225. }
  226. },
  227. [active, selectHandler]
  228. );
  229. const changeHandler = useCallback(
  230. (evt: ChangeEvent<HTMLInputElement>) => {
  231. if (active) {
  232. const { id: key = "" } = (evt.currentTarget as HTMLElement).parentElement?.dataset || {};
  233. selectHandler(key);
  234. }
  235. },
  236. [active, selectHandler]
  237. );
  238. const handleChange = useCallback(
  239. (event: SelectChangeEvent<typeof selectedValue>) => {
  240. const {
  241. target: { value },
  242. } = event;
  243. setSelectedValue(Array.isArray(value) ? value : [value]);
  244. dispatch(
  245. createSendUpdateAction(
  246. updateVarName,
  247. value,
  248. module,
  249. props.onChange,
  250. propagate,
  251. valueById ? undefined : getUpdateVar(updateVars, "lov")
  252. )
  253. );
  254. },
  255. [dispatch, updateVarName, propagate, updateVars, valueById, props.onChange, module]
  256. );
  257. const [autoValue, setAutoValue] = useState<LovItem | LovItem[] | null>(() => (multiple ? [] : null));
  258. const handleAutoChange = useCallback(
  259. (e: SyntheticEvent, sel: LovItem | LovItem[] | null) => {
  260. setAutoValue(sel);
  261. setSelectedValue(Array.isArray(sel) ? sel.map((item) => item.id) : sel ? [sel.id] : []);
  262. dispatch(
  263. createSendUpdateAction(
  264. updateVarName,
  265. Array.isArray(sel) ? sel.map((item) => item.id) : sel?.id,
  266. module,
  267. props.onChange,
  268. propagate,
  269. valueById ? undefined : getUpdateVar(updateVars, "lov")
  270. )
  271. );
  272. },
  273. [dispatch, updateVarName, propagate, updateVars, valueById, props.onChange, module]
  274. );
  275. const handleDelete = useCallback(
  276. (e: React.SyntheticEvent) => {
  277. const id = e.currentTarget?.parentElement?.getAttribute("data-id");
  278. id &&
  279. setSelectedValue((oldKeys) => {
  280. const keys = oldKeys.filter((valId) => valId !== id);
  281. dispatch(
  282. createSendUpdateAction(
  283. updateVarName,
  284. keys,
  285. module,
  286. props.onChange,
  287. propagate,
  288. valueById ? undefined : getUpdateVar(updateVars, "lov")
  289. )
  290. );
  291. return keys;
  292. });
  293. },
  294. [updateVarName, propagate, dispatch, updateVars, valueById, props.onChange, module]
  295. );
  296. const handleInput = useCallback((e: React.ChangeEvent<HTMLInputElement>) => setSearchValue(e.target.value), []);
  297. const dropdownValue = ((dropdown || isRadio) &&
  298. (multiple ? selectedValue : selectedValue.length ? selectedValue[0] : "")) as string[];
  299. return isRadio || isCheck ? (
  300. <FormControl sx={controlSx} className={className}>
  301. {props.label ? <FormLabel>{props.label}</FormLabel> : null}
  302. <Tooltip title={hover || ""}>
  303. {isRadio ? (
  304. <RadioGroup
  305. value={dropdownValue}
  306. onChange={handleChange}
  307. className={getSuffixedClassNames(className, "-radio-group")}
  308. >
  309. {lovList.map((item) => (
  310. <FormControlLabel
  311. key={item.id}
  312. value={item.id}
  313. control={<Radio />}
  314. label={
  315. typeof item.item === "string" ? item.item : <LovImage item={item.item as Icon} />
  316. }
  317. style={getStyles(item.id, selectedValue, theme)}
  318. disabled={!active}
  319. />
  320. ))}
  321. </RadioGroup>
  322. ) : (
  323. <FormGroup className={getSuffixedClassNames(className, "-check-group")}>
  324. {lovList.map((item) => (
  325. <FormControlLabel
  326. key={item.id}
  327. control={
  328. <Checkbox
  329. data-id={item.id}
  330. checked={selectedValue.includes(item.id)}
  331. onChange={changeHandler}
  332. />
  333. }
  334. label={
  335. typeof item.item === "string" ? item.item : <LovImage item={item.item as Icon} />
  336. }
  337. style={getStyles(item.id, selectedValue, theme)}
  338. disabled={!active}
  339. ></FormControlLabel>
  340. ))}
  341. </FormGroup>
  342. )}
  343. </Tooltip>
  344. </FormControl>
  345. ) : dropdown ? (
  346. filter ? (
  347. <Tooltip title={hover || ""} placement="top">
  348. <Autocomplete
  349. id={id}
  350. disabled={!active}
  351. multiple={multiple}
  352. options={lovList}
  353. value={autoValue}
  354. onChange={handleAutoChange}
  355. getOptionLabel={getOptionLabel}
  356. getOptionKey={getOptionKey}
  357. isOptionEqualToValue={isOptionEqualToValue}
  358. sx={controlSx}
  359. className={className}
  360. renderInput={(params) => <TextField {...params} label={props.label} margin="dense" />}
  361. renderOption={renderOption}
  362. />
  363. </Tooltip>
  364. ) : (
  365. <FormControl sx={controlSx} className={className}>
  366. {props.label ? <InputLabel disableAnimation>{props.label}</InputLabel> : null}
  367. <Tooltip title={hover || ""} placement="top">
  368. <Select
  369. id={id}
  370. multiple={multiple}
  371. value={dropdownValue}
  372. onChange={handleChange}
  373. input={<OutlinedInput label={props.label} />}
  374. disabled={!active}
  375. renderValue={(selected) => (
  376. <Box sx={renderBoxSx}>
  377. {lovList
  378. .filter((it) =>
  379. Array.isArray(selected) ? selected.includes(it.id) : selected === it.id
  380. )
  381. .map((item, idx) => {
  382. if (multiple) {
  383. const chipProps = {} as Record<string, unknown>;
  384. if (typeof item.item === "string") {
  385. chipProps.label = item.item;
  386. } else {
  387. chipProps.label = item.item.text || "";
  388. chipProps.avatar = <Avatar src={item.item.path} />;
  389. }
  390. return (
  391. <Chip
  392. key={item.id}
  393. {...chipProps}
  394. onDelete={handleDelete}
  395. data-id={item.id}
  396. onMouseDown={doNotPropagateEvent}
  397. disabled={!active}
  398. />
  399. );
  400. } else if (idx === 0) {
  401. return typeof item.item === "string" ? (
  402. item.item
  403. ) : (
  404. <LovImage item={item.item} />
  405. );
  406. } else {
  407. return null;
  408. }
  409. })}
  410. </Box>
  411. )}
  412. MenuProps={getMenuProps(height)}
  413. >
  414. {lovList.map((item) => (
  415. <MenuItem
  416. key={item.id}
  417. value={item.id}
  418. style={getStyles(item.id, selectedValue, theme)}
  419. disabled={item.id === null}
  420. >
  421. {typeof item.item === "string" ? item.item : <LovImage item={item.item as Icon} />}
  422. </MenuItem>
  423. ))}
  424. </Select>
  425. </Tooltip>
  426. </FormControl>
  427. )
  428. ) : (
  429. <FormControl sx={controlSx} className={className}>
  430. {props.label ? (
  431. <InputLabel disableAnimation className="static-label">
  432. {props.label}
  433. </InputLabel>
  434. ) : null}
  435. <Tooltip title={hover || ""}>
  436. <Paper sx={paperSx}>
  437. {filter && (
  438. <Box>
  439. <OutlinedInput
  440. margin="dense"
  441. placeholder="Search field"
  442. value={searchValue}
  443. onChange={handleInput}
  444. disabled={!active}
  445. />
  446. </Box>
  447. )}
  448. <List sx={listSx} id={id}>
  449. {lovList
  450. .filter((elt) => showItem(elt, searchValue))
  451. .map((elt) =>
  452. multiple ? (
  453. <MultipleItem
  454. key={elt.id}
  455. value={elt.id}
  456. item={elt.item}
  457. selectedValue={selectedValue}
  458. clickHandler={clickHandler}
  459. disabled={!active}
  460. />
  461. ) : (
  462. <SingleItem
  463. key={elt.id}
  464. value={elt.id}
  465. item={elt.item}
  466. selectedValue={selectedValue}
  467. clickHandler={clickHandler}
  468. disabled={!active}
  469. />
  470. )
  471. )}
  472. </List>
  473. </Paper>
  474. </Tooltip>
  475. </FormControl>
  476. );
  477. };
  478. export default Selector;