@nocobase/flow-engine
Version:
A standalone flow engine for NocoBase, managing workflows, models, and actions.
525 lines (523 loc) • 20.8 kB
JavaScript
/**
* 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 ;
overflow-y: auto ;
}
`;
}, [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;