UNPKG

@nocobase/flow-engine

Version:

A standalone flow engine for NocoBase, managing workflows, models, and actions.

525 lines (523 loc) 20.8 kB
/** * This file is part of the NocoBase (R) project. * Copyright (c) 2020-2024 NocoBase Co., Ltd. * Authors: NocoBase Team. * * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. * For more information, please refer to: https://www.nocobase.com/agreement. */ var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var LazyDropdown_exports = {}; __export(LazyDropdown_exports, { default: () => LazyDropdown_default }); module.exports = __toCommonJS(LazyDropdown_exports); var import_css = require("@emotion/css"); var import_antd = require("antd"); var import_react = __toESM(require("react")); var import_provider = require("../../provider"); const useNiceDropdownMaxHeight = /* @__PURE__ */ __name(() => { return (0, import_react.useMemo)(() => { const maxHeight = Math.min(window.innerHeight * 0.6, 400); return maxHeight; }, []); }, "useNiceDropdownMaxHeight"); const useAsyncMenuItems = /* @__PURE__ */ __name((menuVisible, rootItems, resetKey, openKeySet, refreshKeys) => { const [loadedChildren, setLoadedChildren] = (0, import_react.useState)({}); const [loadingKeys, setLoadingKeys] = (0, import_react.useState)(/* @__PURE__ */ new Set()); const lastRefreshedVersion = (0, import_react.useRef)(null); const handleLoadChildren = (0, import_react.useCallback)( async (keyPath, loader, force = false) => { if (loadingKeys.has(keyPath)) return; if (!force && loadedChildren[keyPath]) return; setLoadingKeys((prev) => new Set(prev).add(keyPath)); try { const children = loader(); const resolved = children instanceof Promise ? await children : children; setLoadedChildren((prev) => ({ ...prev, [keyPath]: resolved })); } catch (err) { console.error(`Failed to load children for ${keyPath}`, err); } finally { setLoadingKeys((prev) => { const next = new Set(prev); next.delete(keyPath); return next; }); } }, [loadedChildren, loadingKeys] ); const collectAsyncNodes = /* @__PURE__ */ __name((root, loaded) => { const result = []; const scan = /* @__PURE__ */ __name((items, path = []) => { for (const item of items) { const keyPath = getKeyPath(path, item.key); if (typeof item.children === "function") { result.push([keyPath, item.children]); } if (Array.isArray(item.children)) { scan(item.children, [...path, item.key]); } } }, "scan"); scan(root, []); for (const keyPath of Object.keys(loaded)) { const items = loaded[keyPath] || []; const parts = keyPath ? keyPath.split("/") : []; scan(items, parts); } return result; }, "collectAsyncNodes"); (0, import_react.useEffect)(() => { if (!menuVisible) return; if (lastRefreshedVersion.current === resetKey) { return; } lastRefreshedVersion.current = resetKey; const loaderMap = /* @__PURE__ */ new Map(); for (const [kp, loader] of collectAsyncNodes(rootItems, loadedChildren)) { loaderMap.set(kp, loader); } const openKeys = openKeySet ? Array.from(openKeySet) : []; let targetKeys = []; const candidates = Array.from(new Set([...refreshKeys || []].filter(Boolean))); if (candidates.length > 0) { for (const rk of candidates) { const parts = rk.split("/").filter(Boolean); for (let i = 1; i <= parts.length; i++) { const prefix = parts.slice(0, i).join("/"); if (!targetKeys.includes(prefix) && loadedChildren[prefix]) targetKeys.push(prefix); } } } else { targetKeys = Object.keys(loadedChildren).filter( (kp) => openKeys.some((ok) => kp === ok || kp.startsWith(ok + "/")) ); } if (targetKeys.length === 0) { targetKeys = Object.keys(loadedChildren).filter((kp) => kp.indexOf("/") === -1); } const refreshedInThisVersion = /* @__PURE__ */ new Set(); for (const keyPath of targetKeys) { const loader = loaderMap.get(keyPath); if (!loader) continue; try { if (!refreshedInThisVersion.has(keyPath)) { refreshedInThisVersion.add(keyPath); handleLoadChildren(keyPath, loader, true); } } catch (e) { } } }, [menuVisible, resetKey, rootItems, handleLoadChildren, loadedChildren, openKeySet, refreshKeys]); return { loadedChildren, loadingKeys, handleLoadChildren }; }, "useAsyncMenuItems"); const useKeepDropdownOpen = /* @__PURE__ */ __name(() => { const shouldKeepOpenRef = (0, import_react.useRef)(false); const [forceKeepOpen, setForceKeepOpen] = (0, import_react.useState)(false); const timerRef = (0, import_react.useRef)(null); const requestKeepOpen = (0, import_react.useCallback)(() => { shouldKeepOpenRef.current = true; setForceKeepOpen(true); if (timerRef.current) clearTimeout(timerRef.current); timerRef.current = setTimeout(() => { shouldKeepOpenRef.current = false; setForceKeepOpen(false); timerRef.current = null; }, 250); }, []); (0, import_react.useEffect)(() => () => timerRef.current && clearTimeout(timerRef.current), []); const shouldPreventClose = (0, import_react.useCallback)(() => { return shouldKeepOpenRef.current || forceKeepOpen; }, [forceKeepOpen]); return { requestKeepOpen, shouldPreventClose }; }, "useKeepDropdownOpen"); const useMenuSearch = /* @__PURE__ */ __name(() => { const [searchValues, setSearchValues] = (0, import_react.useState)({}); const [isSearching, setIsSearching] = (0, import_react.useState)(false); const searchTimeoutRef = (0, import_react.useRef)(null); const updateSearchValue = /* @__PURE__ */ __name((key, value) => { setIsSearching(true); setSearchValues((prev) => ({ ...prev, [key]: value })); if (searchTimeoutRef.current) { clearTimeout(searchTimeoutRef.current); } searchTimeoutRef.current = setTimeout(() => setIsSearching(false), 300); }, "updateSearchValue"); (0, import_react.useEffect)(() => { return () => { if (searchTimeoutRef.current) { clearTimeout(searchTimeoutRef.current); } }; }, []); return { searchValues, isSearching, updateSearchValue }; }, "useMenuSearch"); const useSubmenuStyles = /* @__PURE__ */ __name((menuVisible, dropdownMaxHeight) => { (0, import_react.useEffect)(() => { if (!menuVisible || dropdownMaxHeight <= 0) return; const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { mutation.addedNodes.forEach((node) => { var _a; if (node.nodeType === Node.ELEMENT_NODE) { const element = node; if ((_a = element.classList) == null ? void 0 : _a.contains("ant-dropdown-menu-submenu-popup")) { requestAnimationFrame(() => { const menuContainer = element.querySelector(".ant-dropdown-menu"); if (menuContainer) { const container = menuContainer; container.style.maxHeight = `${dropdownMaxHeight}px`; container.style.overflowY = "auto"; } }); } } }); }); }); observer.observe(document.body, { childList: true, subtree: false }); const intervalId = setInterval(() => { const submenuPopups = document.querySelectorAll(".ant-dropdown-menu-submenu-popup .ant-dropdown-menu"); submenuPopups.forEach((menu) => { const container = menu; const rect = container.getBoundingClientRect(); if (rect.width > 0 && rect.height > 0) { container.style.maxHeight = `${dropdownMaxHeight}px`; container.style.overflowY = "auto"; } }); }, 200); return () => { observer.disconnect(); clearInterval(intervalId); }; }, [menuVisible, dropdownMaxHeight]); }, "useSubmenuStyles"); const SearchInputWithAutoFocus = /* @__PURE__ */ __name((props) => { const inputRef = (0, import_react.useRef)(null); const { visible, ...rest } = props; (0, import_react.useEffect)(() => { var _a, _b, _c; if (inputRef.current && visible) { const el = inputRef.current.input || ((_a = inputRef.current.resizableTextArea) == null ? void 0 : _a.textArea) || inputRef.current; try { (_b = el == null ? void 0 : el.focus) == null ? void 0 : _b.call(el, { preventScroll: true }); } catch (e) { (_c = el == null ? void 0 : el.focus) == null ? void 0 : _c.call(el); } } }, [visible]); return /* @__PURE__ */ import_react.default.createElement(import_antd.Input, { ref: inputRef, ...rest }); }, "SearchInputWithAutoFocus"); const getKeyPath = /* @__PURE__ */ __name((path, key) => [...path, key].join("/"), "getKeyPath"); const createSearchItem = /* @__PURE__ */ __name((item, searchKey, currentSearchValue, menuVisible, t, updateSearchValue) => ({ key: `${searchKey}-search`, type: "group", label: /* @__PURE__ */ import_react.default.createElement("div", null, /* @__PURE__ */ import_react.default.createElement( SearchInputWithAutoFocus, { visible: menuVisible, variant: "borderless", allowClear: true, placeholder: t(item.searchPlaceholder || "Search"), value: currentSearchValue, onChange: (e) => { e.stopPropagation(); updateSearchValue(searchKey, e.target.value); }, onClick: (e) => e.stopPropagation(), onMouseDown: (e) => { e.stopPropagation(); }, size: "small", style: { width: "100%", paddingLeft: 0, paddingRight: 0 } } )) }), "createSearchItem"); const createEmptyItem = /* @__PURE__ */ __name((itemKey, t) => ({ key: `${itemKey}-empty`, label: /* @__PURE__ */ import_react.default.createElement("div", { style: { padding: "16px", textAlign: "center" } }, /* @__PURE__ */ import_react.default.createElement(import_antd.Empty, { image: import_antd.Empty.PRESENTED_IMAGE_SIMPLE, description: t("No data"), style: { margin: 0 } })), disabled: true }), "createEmptyItem"); const DROPDOWN_PERSIST_TTL_MS = 350; const dropdownPersistRegistry = /* @__PURE__ */ new Map(); const LazyDropdown = /* @__PURE__ */ __name(({ menu, ...props }) => { const engine = (0, import_provider.useFlowEngine)(); const [menuVisible, setMenuVisible] = (0, import_react.useState)(false); const [openKeys, setOpenKeys] = (0, import_react.useState)(/* @__PURE__ */ new Set()); const [rootItems, setRootItems] = (0, import_react.useState)([]); const [rootLoading, setRootLoading] = (0, import_react.useState)(false); const dropdownMaxHeight = useNiceDropdownMaxHeight(); const t = engine.translate.bind(engine); const { items: menuItems, keepDropdownOpen, persistKey, stateVersion, refreshKeys, ...dropdownMenuProps } = menu; const { loadedChildren, loadingKeys, handleLoadChildren } = useAsyncMenuItems( menuVisible, rootItems, stateVersion, openKeys, refreshKeys ); const { searchValues, isSearching, updateSearchValue } = useMenuSearch(); const { requestKeepOpen, shouldPreventClose } = useKeepDropdownOpen(); useSubmenuStyles(menuVisible, dropdownMaxHeight); (0, import_react.useEffect)(() => { if (!persistKey) return; const until = dropdownPersistRegistry.get(persistKey) || 0; if (until > Date.now()) { setMenuVisible(true); } }, [persistKey]); (0, import_react.useEffect)(() => { return () => { if (persistKey && menuVisible) { const until = Date.now() + DROPDOWN_PERSIST_TTL_MS; dropdownPersistRegistry.set(persistKey, until); setTimeout(() => { const v = dropdownPersistRegistry.get(persistKey) || 0; if (v <= Date.now()) dropdownPersistRegistry.delete(persistKey); }, DROPDOWN_PERSIST_TTL_MS + 100); } }; }, [persistKey, menuVisible]); (0, import_react.useEffect)(() => { const loadRootItems = /* @__PURE__ */ __name(async () => { let resolvedItems; if (typeof menuItems === "function") { setRootLoading(true); try { const res = menuItems(); resolvedItems = res instanceof Promise ? await res : res; } finally { setRootLoading(false); } } else { resolvedItems = menuItems; } setRootItems(resolvedItems); }, "loadRootItems"); if (menuVisible) { loadRootItems(); } }, [menuVisible, stateVersion, menuItems]); function buildSearchChildren(children, item, keyPath, path, menuVisible2, resolve) { const searchKey = keyPath; const currentSearchValue = searchValues[searchKey] || ""; const filteredChildren = currentSearchValue ? (/* @__PURE__ */ __name(function deepFilter(items) { const searchText = currentSearchValue.toLowerCase(); const tryString = /* @__PURE__ */ __name((v) => { if (!v) return ""; return typeof v === "string" ? v : String(v); }, "tryString"); return items.map((child) => { const labelStr = tryString(child.label).toLowerCase(); const selfMatch = labelStr.includes(searchText) || child.key && String(child.key).toLowerCase().includes(searchText); if (child.type === "group" && Array.isArray(child.children)) { const nested = deepFilter(child.children); if (selfMatch || nested.length > 0) { return { ...child, children: nested }; } return null; } return selfMatch ? child : null; }).filter(Boolean); }, "deepFilter"))(children) : children; const resolvedFiltered = resolve(filteredChildren, [...path, item.key]); const searchItem = createSearchItem(item, searchKey, currentSearchValue, menuVisible2, t, updateSearchValue); const dividerItem = { key: `${keyPath}-search-divider`, type: "divider" }; if (currentSearchValue && resolvedFiltered.length === 0) { return [searchItem, dividerItem, createEmptyItem(keyPath, t)]; } return [searchItem, dividerItem, ...resolvedFiltered]; } __name(buildSearchChildren, "buildSearchChildren"); const resolveItems = /* @__PURE__ */ __name((items, path = []) => { return items.map((item) => { const keyPath = getKeyPath(path, item.key); const isGroup = item.type === "group"; const hasAsyncChildren = typeof item.children === "function"; const isLoading = loadingKeys.has(keyPath); const loaded = loadedChildren[keyPath]; const shouldLoadChildren = !isGroup && menuVisible && openKeys.has(keyPath) && hasAsyncChildren && !loaded && !isLoading; if (shouldLoadChildren) { handleLoadChildren(keyPath, item.children); } let children; if (hasAsyncChildren) { children = loaded ?? []; } else if (Array.isArray(item.children)) { children = item.children; } if (hasAsyncChildren && !loaded) { children = [ { key: `${keyPath}-loading`, label: /* @__PURE__ */ import_react.default.createElement(import_antd.Spin, { size: "small" }), disabled: true } ]; } if (isGroup) { if (hasAsyncChildren && !loaded && menuVisible) { handleLoadChildren(keyPath, item.children); } let groupChildren = children ? resolveItems(children, [...path, item.key]) : []; if (item.searchable && children) { groupChildren = buildSearchChildren(children, item, keyPath, path, menuVisible, resolveItems); } return { type: "group", key: keyPath, label: typeof item.label === "string" ? t(item.label) : item.label, children: groupChildren }; } if (item.type === "divider") { return { type: "divider", key: keyPath }; } if (item.searchable && children) { return { key: item.key, label: typeof item.label === "string" ? t(item.label) : item.label, onClick: /* @__PURE__ */ __name((info) => { }, "onClick"), onMouseEnter: /* @__PURE__ */ __name(() => { setOpenKeys((prev) => { if (prev.has(keyPath)) return prev; const next = new Set(prev); next.add(keyPath); return next; }); }, "onMouseEnter"), children: buildSearchChildren(children, item, keyPath, path, menuVisible, resolveItems) }; } return { key: keyPath, label: typeof item.label === "string" ? t(item.label) : item.label, onClick: /* @__PURE__ */ __name((info) => { var _a; if (children) { return; } const itemShouldKeepOpen = item.keepDropdownOpen ?? keepDropdownOpen ?? false; if (itemShouldKeepOpen) { requestKeepOpen(); } const extendedInfo = { ...info, item: info.item || item, originalItem: item, keepDropdownOpen: itemShouldKeepOpen }; (_a = menu.onClick) == null ? void 0 : _a.call(menu, extendedInfo); }, "onClick"), onMouseEnter: /* @__PURE__ */ __name(() => { setOpenKeys((prev) => { if (prev.has(keyPath)) return prev; const next = new Set(prev); next.add(keyPath); return next; }); }, "onMouseEnter"), children: children && children.length > 0 ? resolveItems(children, [...path, item.key]) : children && children.length === 0 ? [createEmptyItem(keyPath, t)] : void 0 }; }); }, "resolveItems"); const overlayClassName = (0, import_react.useMemo)(() => { return import_css.css` .ant-dropdown-menu { max-height: ${dropdownMaxHeight}px; overflow-y: auto; } .ant-dropdown-menu-submenu-popup .ant-dropdown-menu, .ant-dropdown-menu-submenu-popup .ant-dropdown-menu-submenu-popup .ant-dropdown-menu { max-height: ${dropdownMaxHeight}px !important; overflow-y: auto !important; } `; }, [dropdownMaxHeight]); return /* @__PURE__ */ import_react.default.createElement( import_antd.Dropdown, { ...props, open: menuVisible, destroyPopupOnHide: true, overlayClassName, placement: "bottomLeft", menu: { ...dropdownMenuProps, items: rootLoading && rootItems.length === 0 ? [ { key: "root-loading", label: /* @__PURE__ */ import_react.default.createElement(import_antd.Spin, { size: "small" }), disabled: true } ] : resolveItems(rootItems), onClick: /* @__PURE__ */ __name(() => { }, "onClick"), style: { maxHeight: dropdownMaxHeight, overflowY: "auto", ...dropdownMenuProps == null ? void 0 : dropdownMenuProps.style } }, onOpenChange: (visible) => { if (!visible && isSearching) { return; } if (!visible && shouldPreventClose()) { return; } setMenuVisible(visible); } }, props.children ); }, "LazyDropdown"); var LazyDropdown_default = LazyDropdown;