profile-more.tsx 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. import { useState } from "react";
  2. import { useTranslation } from "react-i18next";
  3. import { useLockFn } from "ahooks";
  4. import {
  5. Box,
  6. Badge,
  7. Chip,
  8. Typography,
  9. MenuItem,
  10. Menu,
  11. IconButton,
  12. CircularProgress,
  13. } from "@mui/material";
  14. import { FeaturedPlayListRounded } from "@mui/icons-material";
  15. import { viewProfile } from "@/services/cmds";
  16. import { Notice } from "@/components/base";
  17. import { EditorViewer } from "@/components/profile/editor-viewer";
  18. import { ProfileBox } from "./profile-box";
  19. import { LogViewer } from "./log-viewer";
  20. import { ConfirmViewer } from "./confirm-viewer";
  21. interface Props {
  22. selected: boolean;
  23. activating: boolean;
  24. itemData: IProfileItem;
  25. enableNum: number;
  26. logInfo?: [string, string][];
  27. onEnable: () => void;
  28. onDisable: () => void;
  29. onMoveTop: () => void;
  30. onMoveEnd: () => void;
  31. onEdit: () => void;
  32. onChange?: (prev?: string, curr?: string) => void;
  33. onDelete: () => void;
  34. }
  35. // profile enhanced item
  36. export const ProfileMore = (props: Props) => {
  37. const {
  38. selected,
  39. activating,
  40. itemData,
  41. enableNum,
  42. logInfo = [],
  43. onEnable,
  44. onDisable,
  45. onMoveTop,
  46. onMoveEnd,
  47. onDelete,
  48. onEdit,
  49. onChange,
  50. } = props;
  51. const { uid, type } = itemData;
  52. const { t, i18n } = useTranslation();
  53. const [anchorEl, setAnchorEl] = useState<any>(null);
  54. const [position, setPosition] = useState({ left: 0, top: 0 });
  55. const [fileOpen, setFileOpen] = useState(false);
  56. const [confirmOpen, setConfirmOpen] = useState(false);
  57. const [logOpen, setLogOpen] = useState(false);
  58. const onEditInfo = () => {
  59. setAnchorEl(null);
  60. onEdit();
  61. };
  62. const onEditFile = () => {
  63. setAnchorEl(null);
  64. setFileOpen(true);
  65. };
  66. const onOpenFile = useLockFn(async () => {
  67. setAnchorEl(null);
  68. try {
  69. await viewProfile(itemData.uid);
  70. } catch (err: any) {
  71. Notice.error(err?.message || err.toString());
  72. }
  73. });
  74. const fnWrapper = (fn: () => void) => () => {
  75. setAnchorEl(null);
  76. return fn();
  77. };
  78. const hasError = !!logInfo.find((e) => e[0] === "exception");
  79. const showMove = enableNum > 1 && !hasError;
  80. const enableMenu = [
  81. { label: "Disable", handler: fnWrapper(onDisable) },
  82. { label: "Edit Info", handler: onEditInfo },
  83. { label: "Edit File", handler: onEditFile },
  84. { label: "Open File", handler: onOpenFile },
  85. { label: "To Top", show: showMove, handler: fnWrapper(onMoveTop) },
  86. { label: "To End", show: showMove, handler: fnWrapper(onMoveEnd) },
  87. {
  88. label: "Delete",
  89. handler: () => {
  90. setAnchorEl(null);
  91. setConfirmOpen(true);
  92. },
  93. },
  94. ];
  95. const disableMenu = [
  96. { label: "Enable", handler: fnWrapper(onEnable) },
  97. { label: "Edit Info", handler: onEditInfo },
  98. { label: "Edit File", handler: onEditFile },
  99. { label: "Open File", handler: onOpenFile },
  100. {
  101. label: "Delete",
  102. handler: () => {
  103. setAnchorEl(null);
  104. setConfirmOpen(true);
  105. },
  106. },
  107. ];
  108. const boxStyle = {
  109. height: 26,
  110. display: "flex",
  111. alignItems: "center",
  112. justifyContent: "space-between",
  113. lineHeight: 1,
  114. };
  115. return (
  116. <>
  117. <ProfileBox
  118. aria-selected={selected}
  119. onDoubleClick={onEditFile}
  120. // onClick={() => onSelect(false)}
  121. onContextMenu={(event) => {
  122. const { clientX, clientY } = event;
  123. setPosition({ top: clientY, left: clientX });
  124. setAnchorEl(event.currentTarget);
  125. event.preventDefault();
  126. }}
  127. >
  128. {activating && (
  129. <Box
  130. sx={{
  131. position: "absolute",
  132. display: "flex",
  133. justifyContent: "center",
  134. alignItems: "center",
  135. top: 10,
  136. left: 10,
  137. right: 10,
  138. bottom: 2,
  139. zIndex: 10,
  140. backdropFilter: "blur(2px)",
  141. }}
  142. >
  143. <CircularProgress color="inherit" size={20} />
  144. </Box>
  145. )}
  146. <Box
  147. display="flex"
  148. justifyContent="space-between"
  149. alignItems="center"
  150. mb={0.5}
  151. >
  152. <Typography
  153. width="calc(100% - 52px)"
  154. variant="h6"
  155. component="h2"
  156. noWrap
  157. title={itemData.name}
  158. >
  159. {itemData.name}
  160. </Typography>
  161. <Chip
  162. label={type}
  163. color="primary"
  164. size="small"
  165. variant="outlined"
  166. sx={{ height: 20, textTransform: "capitalize" }}
  167. />
  168. </Box>
  169. <Box sx={boxStyle}>
  170. {selected && type === "script" ? (
  171. hasError ? (
  172. <Badge color="error" variant="dot" overlap="circular">
  173. <IconButton
  174. size="small"
  175. edge="start"
  176. color="error"
  177. title={t("Script Console")}
  178. onClick={() => setLogOpen(true)}
  179. >
  180. <FeaturedPlayListRounded fontSize="inherit" />
  181. </IconButton>
  182. </Badge>
  183. ) : (
  184. <IconButton
  185. size="small"
  186. edge="start"
  187. color="inherit"
  188. title={t("Script Console")}
  189. onClick={() => setLogOpen(true)}
  190. >
  191. <FeaturedPlayListRounded fontSize="inherit" />
  192. </IconButton>
  193. )
  194. ) : (
  195. <Typography
  196. noWrap
  197. title={itemData.desc}
  198. sx={i18n.language === "zh" ? { width: "calc(100% - 75px)" } : {}}
  199. >
  200. {itemData.desc}
  201. </Typography>
  202. )}
  203. </Box>
  204. </ProfileBox>
  205. <Menu
  206. open={!!anchorEl}
  207. anchorEl={anchorEl}
  208. onClose={() => setAnchorEl(null)}
  209. anchorPosition={position}
  210. anchorReference="anchorPosition"
  211. transitionDuration={225}
  212. MenuListProps={{ sx: { py: 0.5 } }}
  213. onContextMenu={(e) => {
  214. setAnchorEl(null);
  215. e.preventDefault();
  216. }}
  217. >
  218. {(selected ? enableMenu : disableMenu)
  219. .filter((item: any) => item.show !== false)
  220. .map((item) => (
  221. <MenuItem
  222. key={item.label}
  223. onClick={item.handler}
  224. sx={[
  225. { minWidth: 120 },
  226. (theme) => {
  227. return {
  228. color:
  229. item.label === "Delete"
  230. ? theme.palette.error.main
  231. : undefined,
  232. };
  233. },
  234. ]}
  235. dense
  236. >
  237. {t(item.label)}
  238. </MenuItem>
  239. ))}
  240. </Menu>
  241. <EditorViewer
  242. mode="profile"
  243. property={uid}
  244. open={fileOpen}
  245. language={type === "merge" ? "yaml" : "javascript"}
  246. schema={type === "merge" ? "merge" : undefined}
  247. onChange={onChange}
  248. onClose={() => setFileOpen(false)}
  249. />
  250. <ConfirmViewer
  251. title={t("Confirm deletion")}
  252. message={t("This operation is not reversible")}
  253. open={confirmOpen}
  254. onClose={() => setConfirmOpen(false)}
  255. onConfirm={() => {
  256. onDelete();
  257. setConfirmOpen(false);
  258. }}
  259. />
  260. {selected && (
  261. <LogViewer
  262. open={logOpen}
  263. logInfo={logInfo}
  264. onClose={() => setLogOpen(false)}
  265. />
  266. )}
  267. </>
  268. );
  269. };