1
0

StatusList.tsx 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  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, { MouseEvent, useCallback, useEffect, useMemo, useState } from "react";
  14. import Box from "@mui/material/Box";
  15. import Stack from "@mui/material/Stack";
  16. import ArrowDownward from "@mui/icons-material/ArrowDownward";
  17. import ArrowUpward from "@mui/icons-material/ArrowUpward";
  18. import Tooltip from "@mui/material/Tooltip";
  19. import Popover, { PopoverOrigin } from "@mui/material/Popover";
  20. import Status, { StatusType } from "./Status";
  21. import { getSuffixedClassNames, TaipyBaseProps, TaipyHoverProps } from "./utils";
  22. import { useClassNames, useDynamicProperty } from "../../utils/hooks";
  23. import { getComponentClassName } from "./TaipyStyle";
  24. export const getStatusIntValue = (status: string) => {
  25. status = status.toLowerCase();
  26. if (status.startsWith("i")) {
  27. return 0;
  28. } else if (status.startsWith("s")) {
  29. return 1;
  30. } else if (status.startsWith("w")) {
  31. return 2;
  32. } else if (status.startsWith("e")) {
  33. return 3;
  34. }
  35. return -1;
  36. };
  37. export const getStatusStrValue = (status: number) => {
  38. switch (status) {
  39. case 0:
  40. return "info";
  41. case 1:
  42. return "success";
  43. case 2:
  44. return "warning";
  45. case 3:
  46. return "error";
  47. default:
  48. return "unknown";
  49. }
  50. };
  51. const getId = (base: string | undefined, idx: number) => (base || "status") + idx;
  52. const NO_STATUS = { status: "info", message: "No Status" };
  53. const getGlobalStatus = (values: StatusDel[]) => {
  54. values = values.filter((val) => !val.hidden);
  55. if (values.length == 0) {
  56. return NO_STATUS;
  57. } else if (values.length == 1) {
  58. return values[0];
  59. } else {
  60. const status = values.reduce((prevVal, currentStatus) => {
  61. const newVal = getStatusIntValue(currentStatus.status);
  62. return prevVal > newVal ? prevVal : newVal;
  63. }, 0);
  64. return { status: getStatusStrValue(status), message: `${values.length} statuses` };
  65. }
  66. };
  67. const statusEqual = (v1: StatusDel, v2: StatusDel) => v1.status === v2.status && v1.message === v2.message;
  68. const getIcon = (icons: Array<boolean|string>, index: number) => index >= 0 && index < icons.length ? icons[index] : false;
  69. const ORIGIN = {
  70. vertical: "bottom",
  71. horizontal: "left",
  72. } as PopoverOrigin;
  73. interface StatusDel extends StatusType {
  74. hidden?: boolean;
  75. id?: string;
  76. }
  77. interface StatusListProps extends TaipyBaseProps, TaipyHoverProps {
  78. value: Array<[string, string] | StatusType> | [string, string] | StatusType;
  79. defaultValue?: string;
  80. withoutClose?: boolean;
  81. useIcon?: boolean | string;
  82. }
  83. const StatusList = (props: StatusListProps) => {
  84. const { value, defaultValue, withoutClose = false } = props;
  85. const [values, setValues] = useState<StatusDel[]>([]);
  86. const [opened, setOpened] = useState(false);
  87. const [multiple, setMultiple] = useState(false);
  88. const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
  89. const className = useClassNames(props.libClassName, props.dynamicClassName, props.className);
  90. const hover = useDynamicProperty(props.hoverText, props.defaultHoverText, undefined);
  91. const icons = useMemo(() => {
  92. if (typeof props.useIcon === "string") {
  93. try {
  94. const iconsDict = JSON.parse(props.useIcon);
  95. const defaultVal = iconsDict.__default !== undefined ? iconsDict.__default : false;
  96. const res = [defaultVal, defaultVal, defaultVal, defaultVal];
  97. Object.entries(iconsDict).forEach(([k, v]) => {
  98. const idx = getStatusIntValue(k);
  99. if (idx >=0) {
  100. res[idx] = v;
  101. }
  102. });
  103. return res;
  104. } catch (e) {
  105. console.info(`Error parsing icons\n${(e as Error).message || e}`);
  106. }
  107. return [false, false, false, false];
  108. }
  109. return [!!props.useIcon, !!props.useIcon, !!props.useIcon, !!props.useIcon];
  110. }, [props.useIcon]);
  111. useEffect(() => {
  112. let val;
  113. if (value === undefined) {
  114. try {
  115. val = (defaultValue ? JSON.parse(defaultValue) : []) as StatusType[] | StatusType;
  116. } catch (e) {
  117. console.info(`Cannot parse status value: ${(e as Error).message || e}`);
  118. val = [] as StatusType[];
  119. }
  120. } else {
  121. val = value;
  122. }
  123. if (!Array.isArray(val) || (val.length && typeof val[0] !== "object")) {
  124. val = [val];
  125. }
  126. val = val.map((v) => {
  127. if (Array.isArray(v)) {
  128. return { status: v[0] || "", message: v[1] || "" };
  129. } else if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
  130. return { status: "" + v, message: "" + v };
  131. } else {
  132. return { status: v.status || "", message: v.message || "" };
  133. }
  134. });
  135. setValues(val as StatusDel[]);
  136. setMultiple(val.length > 1);
  137. }, [value, defaultValue]);
  138. const onClose = useCallback((val: StatusDel) => {
  139. setValues((values) => {
  140. const res = values.map((v) => {
  141. if (!v.hidden && statusEqual(v, val)) {
  142. v.hidden = !v.hidden;
  143. }
  144. return v;
  145. });
  146. if (res.filter((v) => !v.hidden).length < 2) {
  147. setOpened(false);
  148. setMultiple(false);
  149. }
  150. return res;
  151. });
  152. }, []);
  153. const onOpen = useCallback((evt: MouseEvent) => {
  154. setOpened((op) => {
  155. setAnchorEl(op ? null : (evt.currentTarget || (evt.target as HTMLElement)).parentElement);
  156. return !op;
  157. });
  158. }, []);
  159. const globalProps = useMemo(
  160. () => (multiple ? { onClose: onOpen, openedIcon: opened ? <ArrowUpward /> : <ArrowDownward /> } : {}),
  161. [multiple, opened, onOpen]
  162. );
  163. const globStatus = getGlobalStatus(values);
  164. return (
  165. <Tooltip title={hover || ""}>
  166. <Box className={`${className} ${getComponentClassName(props.children)}`}>
  167. <Status
  168. id={props.id}
  169. value={globStatus}
  170. className={getSuffixedClassNames(className, "-main")}
  171. {...globalProps}
  172. icon={getIcon(icons, getStatusIntValue(globStatus.status))}
  173. />
  174. <Popover open={opened} anchorEl={anchorEl} onClose={onOpen} anchorOrigin={ORIGIN}>
  175. <Stack direction="column" spacing={1}>
  176. {values
  177. .filter((val) => !val.hidden)
  178. .map((val, idx) => {
  179. const closeProp = withoutClose ? {} : { onClose: () => onClose(val) };
  180. return (
  181. <Status
  182. key={getId(props.id, idx)}
  183. id={getId(props.id, idx)}
  184. value={val}
  185. className={getSuffixedClassNames(className, `-${idx}`)}
  186. {...closeProp}
  187. icon={getIcon(icons, getStatusIntValue(val.status))}
  188. />
  189. );
  190. })}
  191. </Stack>
  192. </Popover>
  193. {props.children}
  194. </Box>
  195. </Tooltip>
  196. );
  197. };
  198. export default StatusList;