UNPKG

@nocobase/flow-engine

Version:

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

502 lines (500 loc) 19.2 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 DefaultSettingsIcon_exports = {}; __export(DefaultSettingsIcon_exports, { DefaultSettingsIcon: () => DefaultSettingsIcon }); module.exports = __toCommonJS(DefaultSettingsIcon_exports); var import_icons = require("@ant-design/icons"); var import_antd = require("antd"); var import_react = __toESM(require("react")); var import_models = require("../../../../models"); var import_utils = require("../../../../utils"); var import_hooks = require("../../../../hooks"); const findSubModelByKey = /* @__PURE__ */ __name((model, subModelKey) => { var _a; if (!model || !subModelKey || typeof subModelKey !== "string") { console.warn("Invalid input parameters"); return null; } const match = subModelKey.match(/^([a-zA-Z_$][a-zA-Z0-9_$]*)(?:\[(\d+)\])?$/); if (!match) { console.warn(`Invalid subModelKey format: ${subModelKey}`); return null; } const [, subKey, indexStr] = match; const subModel = (_a = model.subModels) == null ? void 0 : _a[subKey]; if (!subModel) { console.warn(`SubModel '${subKey}' not found`); return null; } if (indexStr !== void 0) { const index = parseInt(indexStr, 10); if (!Array.isArray(subModel)) { console.warn(`Expected array for '${subKey}', got ${typeof subModel}`); return null; } if (index < 0 || index >= subModel.length) { console.warn(`Array index ${index} out of bounds for '${subKey}'`); return null; } return subModel[index] instanceof import_models.FlowModel ? subModel[index] : null; } else { if (Array.isArray(subModel)) { console.warn(`Expected object for '${subKey}', got array`); return null; } return subModel instanceof import_models.FlowModel ? subModel : null; } }, "findSubModelByKey"); const DefaultSettingsIcon = /* @__PURE__ */ __name(({ model, showDeleteButton = true, showCopyUidButton = true, menuLevels = 1, // 默认一级菜单 flattenSubMenus = true }) => { const { message } = import_antd.App.useApp(); const t = (0, import_utils.getT)(model); const [visible, setVisible] = (0, import_react.useState)(false); const handleOpenChange = (0, import_react.useCallback)((nextOpen, info) => { if (info.source === "trigger" || nextOpen) { (0, import_react.startTransition)(() => { setVisible(nextOpen); }); } }, []); const dropdownMaxHeight = (0, import_hooks.useNiceDropdownMaxHeight)([visible]); const copyUidToClipboard = (0, import_react.useCallback)( async (uid) => { try { await navigator.clipboard.writeText(uid); message.success(t("UID copied to clipboard")); } catch (error) { console.error(t("Copy failed"), ":", error); message.error(t("Copy failed, please copy [{{uid}}] manually.", { uid })); } }, [message, t] ); const handleCopyUid = (0, import_react.useCallback)(async () => { copyUidToClipboard(model.uid); }, [model.uid, copyUidToClipboard]); const handleCopyPopupUid = (0, import_react.useCallback)( (menuKey) => { try { const originalKey = menuKey.replace(/^copy-pop-uid:/, ""); const keyParts = originalKey.split(":"); let targetModel = model; if (keyParts.length === 3) { const [subModelKey] = keyParts; targetModel = findSubModelByKey(model, subModelKey) || model; } else if (keyParts.length !== 2) { console.error("Invalid copy-pop-uid key format:", menuKey); return; } copyUidToClipboard(targetModel.uid); } catch (error) { console.error("handleCopyPopupUid error:", error); } }, [model, copyUidToClipboard] ); const handleDelete = (0, import_react.useCallback)(() => { import_antd.Modal.confirm({ title: t("Confirm delete"), icon: /* @__PURE__ */ import_react.default.createElement(import_icons.ExclamationCircleOutlined, null), content: t("Are you sure you want to delete this item? This action cannot be undone."), okText: t("Confirm"), okType: "primary", cancelText: t("Cancel"), zIndex: 999999, async onOk() { try { await model.destroy(); } catch (error) { console.error(t("Delete operation failed"), ":", error); import_antd.Modal.error({ title: t("Delete failed"), content: t("Delete operation failed, please check the console for details.") }); } } }); }, [model]); const handleStepConfiguration = (0, import_react.useCallback)( (key) => { const keyParts = key.split(":"); if (keyParts.length < 2 || keyParts.length > 3) { console.error("Invalid configuration key format:", key); return; } let targetModel = model; let flowKey; let stepKey; if (keyParts.length === 3) { const [subModelKey, subFlowKey, subStepKey] = keyParts; flowKey = subFlowKey; stepKey = subStepKey; const subModel = findSubModelByKey(model, subModelKey); if (!subModel) { console.error(`Sub-model '${subModelKey}' not found`); return; } targetModel = subModel; } else { [flowKey, stepKey] = keyParts; } try { targetModel.openFlowSettings({ flowKey, stepKey }); } catch (error) { console.log(t("Configuration popup cancelled or error"), ":", error); } }, [model] ); const handleMenuClick = (0, import_react.useCallback)( ({ key }) => { const cleanKey = key.includes("-") && /^(.+)-\d+$/.test(key) ? key.replace(/-\d+$/, "") : key; if (cleanKey.startsWith("copy-pop-uid:")) { handleCopyPopupUid(cleanKey); return; } switch (cleanKey) { case "copy-uid": handleCopyUid(); break; case "delete": handleDelete(); break; default: handleStepConfiguration(cleanKey); break; } }, [handleCopyUid, handleDelete, handleStepConfiguration, handleCopyPopupUid] ); const getModelConfigurableFlowsAndSteps = (0, import_react.useCallback)( async (targetModel, modelKey) => { try { const flows = targetModel.getFlows(); const flowsArray = Array.from(flows.values()); const flowsWithSteps = await Promise.all( flowsArray.map(async (flow) => { const configurableSteps = await Promise.all( Object.entries(flow.steps).map(async ([stepKey, stepDefinition]) => { var _a, _b; const actionStep = stepDefinition; if (actionStep.hideInSettings) { return null; } const hasStepUiSchema = actionStep.uiSchema != null; let hasActionUiSchema = false; let stepTitle = actionStep.title; if (actionStep.use) { try { const action = (_b = (_a = targetModel.flowEngine) == null ? void 0 : _a.getAction) == null ? void 0 : _b.call(_a, actionStep.use); hasActionUiSchema = action && action.uiSchema != null; stepTitle = stepTitle || action.title; } catch (error) { console.warn(t("Failed to get action {{action}}", { action: actionStep.use }), ":", error); } } if (!hasStepUiSchema && !hasActionUiSchema) { return null; } let mergedUiSchema = {}; try { const resolvedSchema = await (0, import_utils.resolveStepUiSchema)(targetModel, flow, actionStep); if (!resolvedSchema) { return null; } mergedUiSchema = resolvedSchema; } catch (error) { console.warn(t("Failed to resolve uiSchema for step {{stepKey}}", { stepKey }), ":", error); return null; } return { stepKey, step: actionStep, uiSchema: mergedUiSchema, title: t(stepTitle) || stepKey, modelKey // 添加模型标识 }; }) ).then((steps) => steps.filter(Boolean)); return configurableSteps.length > 0 ? { flow, steps: configurableSteps, modelKey } : null; }) ).then((flows2) => flows2.filter(Boolean)); return flowsWithSteps; } catch (error) { console.error( t("Failed to get configurable flows for model {{model}}", { model: (targetModel == null ? void 0 : targetModel.uid) || "unknown" }), ":", error ); return []; } }, [] ); const getConfigurableFlowsAndSteps = (0, import_react.useCallback)(async () => { const result = []; const processedModels = /* @__PURE__ */ new Set(); const processModel = /* @__PURE__ */ __name(async (targetModel, depth, modelKey) => { if (depth > menuLevels) { return; } const modelId = targetModel.uid || `temp-${Date.now()}`; if (processedModels.has(modelId)) { return; } processedModels.add(modelId); try { const modelFlows = await getModelConfigurableFlowsAndSteps(targetModel, modelKey); result.push(...modelFlows); if (depth < menuLevels && targetModel.subModels) { await Promise.all( Object.entries(targetModel.subModels).map(async ([subKey, subModelValue]) => { if (Array.isArray(subModelValue)) { await Promise.all( subModelValue.map(async (subModel, index) => { if (subModel instanceof import_models.FlowModel && index < 50) { await processModel(subModel, depth + 1, `${subKey}[${index}]`); } }) ); } else if (subModelValue instanceof import_models.FlowModel) { await processModel(subModelValue, depth + 1, subKey); } }) ); } } finally { processedModels.delete(modelId); } }, "processModel"); await processModel(model, 1); return result; }, [model, menuLevels, getModelConfigurableFlowsAndSteps]); const [configurableFlowsAndSteps, setConfigurableFlowsAndSteps] = (0, import_react.useState)([]); const [isLoading, setIsLoading] = (0, import_react.useState)(true); const [refreshTick, setRefreshTick] = (0, import_react.useState)(0); (0, import_react.useEffect)(() => { const triggerRebuild = /* @__PURE__ */ __name(() => setRefreshTick((v) => v + 1), "triggerRebuild"); if (model == null ? void 0 : model.emitter) { model.emitter.on("onSubModelAdded", triggerRebuild); model.emitter.on("onSubModelRemoved", triggerRebuild); model.emitter.on("onSubModelReplaced", triggerRebuild); } return () => { if (model == null ? void 0 : model.emitter) { model.emitter.off("onSubModelAdded", triggerRebuild); model.emitter.off("onSubModelRemoved", triggerRebuild); model.emitter.off("onSubModelReplaced", triggerRebuild); } }; }, [model]); (0, import_react.useEffect)(() => { const loadConfigurableFlowsAndSteps = /* @__PURE__ */ __name(async () => { setIsLoading(true); try { const flows = await getConfigurableFlowsAndSteps(); setConfigurableFlowsAndSteps(flows); } catch (error) { console.error("Failed to load configurable flows and steps:", error); setConfigurableFlowsAndSteps([]); } finally { setIsLoading(false); } }, "loadConfigurableFlowsAndSteps"); loadConfigurableFlowsAndSteps(); }, [getConfigurableFlowsAndSteps, refreshTick]); const menuItems = (0, import_react.useMemo)(() => { const items = []; const keyCounter = /* @__PURE__ */ new Map(); const generateUniqueKey = /* @__PURE__ */ __name((baseKey) => { const count = keyCounter.get(baseKey) || 0; keyCounter.set(baseKey, count + 1); return count === 0 ? baseKey : `${baseKey}-${count}`; }, "generateUniqueKey"); if (configurableFlowsAndSteps.length > 0) { if (flattenSubMenus) { configurableFlowsAndSteps.forEach(({ flow, steps, modelKey }) => { const groupKey = generateUniqueKey(`flow-group-${modelKey ? `${modelKey}-` : ""}${flow.key}`); items.push({ key: groupKey, label: t(flow.title) || flow.key, type: "group" }); steps.forEach((stepInfo) => { const baseMenuKey = modelKey ? `${modelKey}:${flow.key}:${stepInfo.stepKey}` : `${flow.key}:${stepInfo.stepKey}`; const uniqueKey = generateUniqueKey(baseMenuKey); items.push({ key: uniqueKey, label: t(stepInfo.title) }); if (flow.key === "popupSettings") { const copyKey = generateUniqueKey(`copy-pop-uid:${baseMenuKey}`); items.push({ key: copyKey, label: t("Copy popup UID") }); } }); }); } else { const modelGroups = /* @__PURE__ */ new Map(); configurableFlowsAndSteps.forEach((flowInfo) => { const modelKey = flowInfo.modelKey || "current"; if (!modelGroups.has(modelKey)) { modelGroups.set(modelKey, []); } const group = modelGroups.get(modelKey); if (group) { group.push(flowInfo); } }); modelGroups.forEach((flows, modelKey) => { if (modelKey === "current") { flows.forEach(({ flow, steps }) => { const groupKey = generateUniqueKey(`flow-group-${flow.key}`); items.push({ key: groupKey, label: t(flow.title) || flow.key, type: "group" }); steps.forEach((stepInfo) => { const uniqueKey = generateUniqueKey(`${flow.key}:${stepInfo.stepKey}`); items.push({ key: uniqueKey, label: t(stepInfo.title) }); if (flow.key === "popupSettings") { const copyKey = generateUniqueKey(`copy-pop-uid:${flow.key}:${stepInfo.stepKey}`); items.push({ key: copyKey, label: t("Copy popup UID") }); } }); }); } else { const subMenuKey = generateUniqueKey(`sub-menu-${modelKey}`); const subMenuChildren = []; flows.forEach(({ flow, steps }) => { steps.forEach((stepInfo) => { const uniqueKey = generateUniqueKey(`${modelKey}:${flow.key}:${stepInfo.stepKey}`); subMenuChildren.push({ key: uniqueKey, label: t(stepInfo.title) }); if (flow.key === "popupSettings") { const copyKey = generateUniqueKey(`copy-pop-uid:${modelKey}:${flow.key}:${stepInfo.stepKey}`); subMenuChildren.push({ key: copyKey, label: t("Copy popup UID") }); } }); }); items.push({ key: subMenuKey, label: modelKey, children: subMenuChildren }); } }); } } return items; }, [configurableFlowsAndSteps, flattenSubMenus, t]); const finalMenuItems = (0, import_react.useMemo)(() => { const items = [...menuItems]; if (showCopyUidButton || showDeleteButton) { items.push({ key: "common-actions", label: t("Common actions"), type: "group" }); if (showCopyUidButton && model.uid) { items.push({ key: "copy-uid", label: t("Copy UID") }); } if (showDeleteButton && typeof model.destroy === "function") { items.push({ key: "delete", label: t("Delete") }); } } return items; }, [menuItems, showCopyUidButton, showDeleteButton, model.uid, model.destroy, t]); if (isLoading || configurableFlowsAndSteps.length === 0 && !showDeleteButton && !showCopyUidButton) { return null; } if (!model || !model.uid) { console.warn(t("Invalid model provided")); return null; } return /* @__PURE__ */ import_react.default.createElement( import_antd.Dropdown, { onOpenChange: handleOpenChange, open: visible, menu: { items: finalMenuItems, onClick: handleMenuClick, style: { maxHeight: dropdownMaxHeight, overflowY: "auto" } }, trigger: ["hover"], placement: "bottomRight" }, /* @__PURE__ */ import_react.default.createElement(import_icons.MenuOutlined, { role: "button", "aria-label": "flows-settings", style: { cursor: "pointer", fontSize: 12 } }) ); }, "DefaultSettingsIcon"); // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { DefaultSettingsIcon });