@nocobase/flow-engine
Version:
A standalone flow engine for NocoBase, managing workflows, models, and actions.
502 lines (500 loc) • 19.2 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 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
});