@nocobase/flow-engine 2.0.0-alpha.53 → 2.0.0-alpha.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,18 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+ import React from 'react';
10
+ export interface SelectWithTitleProps {
11
+ title?: any;
12
+ getDefaultValue?: any;
13
+ options?: any;
14
+ fieldNames?: any;
15
+ itemKey?: string;
16
+ onChange?: (...args: any[]) => void;
17
+ }
18
+ export declare function SelectWithTitle({ title, getDefaultValue, onChange, options, fieldNames, itemKey, ...others }: SelectWithTitleProps): React.JSX.Element;
@@ -0,0 +1,135 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ var __create = Object.create;
11
+ var __defProp = Object.defineProperty;
12
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
13
+ var __getOwnPropNames = Object.getOwnPropertyNames;
14
+ var __getProtoOf = Object.getPrototypeOf;
15
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
16
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
17
+ var __export = (target, all) => {
18
+ for (var name in all)
19
+ __defProp(target, name, { get: all[name], enumerable: true });
20
+ };
21
+ var __copyProps = (to, from, except, desc) => {
22
+ if (from && typeof from === "object" || typeof from === "function") {
23
+ for (let key of __getOwnPropNames(from))
24
+ if (!__hasOwnProp.call(to, key) && key !== except)
25
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
26
+ }
27
+ return to;
28
+ };
29
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
30
+ // If the importer is in node compatibility mode or this is not an ESM
31
+ // file that has been converted to a CommonJS file using a Babel-
32
+ // compatible transform (i.e. "__esModule" has not been set), then set
33
+ // "default" to the CommonJS "module.exports" for node compatibility.
34
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
35
+ mod
36
+ ));
37
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
38
+ var SelectWithTitle_exports = {};
39
+ __export(SelectWithTitle_exports, {
40
+ SelectWithTitle: () => SelectWithTitle
41
+ });
42
+ module.exports = __toCommonJS(SelectWithTitle_exports);
43
+ var import_antd = require("antd");
44
+ var import_react = __toESM(require("react"));
45
+ var import_provider = require("../../../../provider");
46
+ function SelectWithTitle({
47
+ title,
48
+ getDefaultValue,
49
+ onChange,
50
+ options,
51
+ fieldNames,
52
+ itemKey,
53
+ ...others
54
+ }) {
55
+ const [open, setOpen] = (0, import_react.useState)(false);
56
+ const [value, setValue] = (0, import_react.useState)("");
57
+ const ctx = (0, import_provider.useFlowEngineContext)();
58
+ (0, import_react.useEffect)(() => {
59
+ let cancelled = false;
60
+ const run = /* @__PURE__ */ __name(async () => {
61
+ if (!getDefaultValue) return;
62
+ try {
63
+ const val = await getDefaultValue();
64
+ if (cancelled || !val) return;
65
+ const entries = Object.entries(val);
66
+ if (!entries.length) return;
67
+ const [key, result] = entries[0];
68
+ setValue(result);
69
+ } catch (e) {
70
+ console.error(e);
71
+ }
72
+ }, "run");
73
+ run();
74
+ return () => {
75
+ cancelled = true;
76
+ };
77
+ }, [getDefaultValue]);
78
+ const timerRef = (0, import_react.useRef)(null);
79
+ const handleChange = /* @__PURE__ */ __name((val) => {
80
+ setValue(val);
81
+ onChange == null ? void 0 : onChange({ [itemKey]: val });
82
+ }, "handleChange");
83
+ return /* @__PURE__ */ import_react.default.createElement(
84
+ "div",
85
+ {
86
+ style: { alignItems: "center", display: "flex", justifyContent: "space-between" },
87
+ onClick: (e) => {
88
+ e.stopPropagation();
89
+ setOpen((v) => !v);
90
+ },
91
+ onMouseLeave: () => {
92
+ timerRef.current = setTimeout(() => {
93
+ setOpen(false);
94
+ }, 200);
95
+ }
96
+ },
97
+ /* @__PURE__ */ import_react.default.createElement(
98
+ "span",
99
+ {
100
+ style: {
101
+ whiteSpace: "nowrap",
102
+ // 不换行
103
+ flexShrink: 0
104
+ // 不被挤压
105
+ }
106
+ },
107
+ title
108
+ ),
109
+ /* @__PURE__ */ import_react.default.createElement(
110
+ import_antd.Select,
111
+ {
112
+ ...others,
113
+ open,
114
+ popupMatchSelectWidth: false,
115
+ bordered: false,
116
+ value,
117
+ onChange: handleChange,
118
+ popupClassName: `select-popup-${title.replaceAll(" ", "-")}`,
119
+ fieldNames,
120
+ options,
121
+ labelRender: (props) => ctx.t(props.label),
122
+ optionRender: (o) => ctx.t(o.label),
123
+ style: { textAlign: "right", minWidth: 100 },
124
+ onMouseEnter: () => {
125
+ clearTimeout(timerRef.current);
126
+ }
127
+ }
128
+ )
129
+ );
130
+ }
131
+ __name(SelectWithTitle, "SelectWithTitle");
132
+ // Annotate the CommonJS export names for ESM import in node:
133
+ 0 && (module.exports = {
134
+ SelectWithTitle
135
+ });
@@ -0,0 +1,10 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+ import { FC } from 'react';
10
+ export declare const SwitchWithTitle: FC;
@@ -0,0 +1,110 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ var __create = Object.create;
11
+ var __defProp = Object.defineProperty;
12
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
13
+ var __getOwnPropNames = Object.getOwnPropertyNames;
14
+ var __getProtoOf = Object.getPrototypeOf;
15
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
16
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
17
+ var __export = (target, all) => {
18
+ for (var name in all)
19
+ __defProp(target, name, { get: all[name], enumerable: true });
20
+ };
21
+ var __copyProps = (to, from, except, desc) => {
22
+ if (from && typeof from === "object" || typeof from === "function") {
23
+ for (let key of __getOwnPropNames(from))
24
+ if (!__hasOwnProp.call(to, key) && key !== except)
25
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
26
+ }
27
+ return to;
28
+ };
29
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
30
+ // If the importer is in node compatibility mode or this is not an ESM
31
+ // file that has been converted to a CommonJS file using a Babel-
32
+ // compatible transform (i.e. "__esModule" has not been set), then set
33
+ // "default" to the CommonJS "module.exports" for node compatibility.
34
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
35
+ mod
36
+ ));
37
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
38
+ var SwitchWithTitle_exports = {};
39
+ __export(SwitchWithTitle_exports, {
40
+ SwitchWithTitle: () => SwitchWithTitle
41
+ });
42
+ module.exports = __toCommonJS(SwitchWithTitle_exports);
43
+ var import_react = require("@formily/react");
44
+ var import_antd = require("antd");
45
+ var import_react2 = __toESM(require("react"));
46
+ var import_provider = require("../../../../provider");
47
+ const ml32 = { marginLeft: 32 };
48
+ const SwitchWithTitle = (0, import_react.observer)(
49
+ ({ title, onChange, getDefaultValue, disabled, itemKey, ...others }) => {
50
+ const [checked, setChecked] = (0, import_react2.useState)(false);
51
+ const ctx = (0, import_provider.useFlowEngineContext)();
52
+ (0, import_react2.useEffect)(() => {
53
+ let cancelled = false;
54
+ const run = /* @__PURE__ */ __name(async () => {
55
+ if (!getDefaultValue) return;
56
+ try {
57
+ const val = await getDefaultValue();
58
+ if (cancelled || !val) return;
59
+ const entries = Object.entries(val);
60
+ if (!entries.length) return;
61
+ const [key, value] = entries[0];
62
+ setChecked(!!value);
63
+ } catch (e) {
64
+ console.error(e);
65
+ }
66
+ }, "run");
67
+ run();
68
+ return () => {
69
+ cancelled = true;
70
+ };
71
+ }, [getDefaultValue]);
72
+ const handleChange = /* @__PURE__ */ __name((val) => {
73
+ setChecked(val);
74
+ onChange == null ? void 0 : onChange({ [itemKey]: val });
75
+ }, "handleChange");
76
+ const handleWrapperClick = /* @__PURE__ */ __name(() => {
77
+ if (disabled) return;
78
+ handleChange(!checked);
79
+ }, "handleWrapperClick");
80
+ return /* @__PURE__ */ import_react2.default.createElement(
81
+ "div",
82
+ {
83
+ style: {
84
+ alignItems: "center",
85
+ display: "flex",
86
+ justifyContent: "space-between",
87
+ cursor: disabled ? "not-allowed" : "pointer"
88
+ },
89
+ onClick: handleWrapperClick
90
+ },
91
+ title,
92
+ /* @__PURE__ */ import_react2.default.createElement(
93
+ import_antd.Switch,
94
+ {
95
+ size: "small",
96
+ ...others,
97
+ checkedChildren: others.checkedChildren ? ctx.t(others.checkedChildren) : void 0,
98
+ unCheckedChildren: others.unCheckedChildren ? ctx.t(others.unCheckedChildren) : void 0,
99
+ checked,
100
+ style: ml32,
101
+ disabled
102
+ }
103
+ )
104
+ );
105
+ }
106
+ );
107
+ // Annotate the CommonJS export names for ESM import in node:
108
+ 0 && (module.exports = {
109
+ SwitchWithTitle
110
+ });
@@ -46,6 +46,8 @@ var import_react = __toESM(require("react"));
46
46
  var import_models = require("../../../../models");
47
47
  var import_utils = require("../../../../utils");
48
48
  var import_hooks = require("../../../../hooks");
49
+ var import_SwitchWithTitle = require("../component/SwitchWithTitle");
50
+ var import_SelectWithTitle = require("../component/SelectWithTitle");
49
51
  const findSubModelByKey = /* @__PURE__ */ __name((model, subModelKey) => {
50
52
  var _a;
51
53
  if (!model || !subModelKey || typeof subModelKey !== "string") {
@@ -82,6 +84,18 @@ const findSubModelByKey = /* @__PURE__ */ __name((model, subModelKey) => {
82
84
  return subModel instanceof import_models.FlowModel ? subModel : null;
83
85
  }
84
86
  }, "findSubModelByKey");
87
+ const componentMap = {
88
+ switch: import_SwitchWithTitle.SwitchWithTitle,
89
+ select: import_SelectWithTitle.SelectWithTitle
90
+ };
91
+ const MenuLabelItem = /* @__PURE__ */ __name(({ title, uiMode, itemProps }) => {
92
+ const type = (uiMode == null ? void 0 : uiMode.type) || uiMode;
93
+ const Component = type ? componentMap[type] : null;
94
+ if (!Component) {
95
+ return /* @__PURE__ */ import_react.default.createElement(import_react.default.Fragment, null, title);
96
+ }
97
+ return /* @__PURE__ */ import_react.default.createElement(Component, { title, ...itemProps });
98
+ }, "MenuLabelItem");
85
99
  const DefaultSettingsIcon = /* @__PURE__ */ __name(({
86
100
  model,
87
101
  showDeleteButton = true,
@@ -238,28 +252,33 @@ const DefaultSettingsIcon = /* @__PURE__ */ __name(({
238
252
  Object.entries(flow.steps).map(async ([stepKey, stepDefinition]) => {
239
253
  var _a;
240
254
  const actionStep = stepDefinition;
255
+ let step = actionStep;
241
256
  if (await (0, import_utils.shouldHideStepInSettings)(targetModel, flow, actionStep)) {
242
257
  return null;
243
258
  }
259
+ let uiMode = await (0, import_utils.resolveUiMode)(actionStep.uiMode, targetModel.context);
244
260
  const hasStepUiSchema = actionStep.uiSchema != null;
245
261
  let hasActionUiSchema = false;
246
262
  let stepTitle = actionStep.title;
247
263
  if (actionStep.use) {
248
264
  try {
249
265
  const action = (_a = targetModel.getAction) == null ? void 0 : _a.call(targetModel, actionStep.use);
266
+ step = { ...action || {}, ...actionStep };
267
+ uiMode = await (0, import_utils.resolveUiMode)((action == null ? void 0 : action.uiMode) || uiMode, targetModel.context);
250
268
  hasActionUiSchema = action && action.uiSchema != null;
251
269
  stepTitle = stepTitle || (action == null ? void 0 : action.title);
252
270
  } catch (error) {
253
271
  console.warn(t("Failed to get action {{action}}", { action: actionStep.use }), ":", error);
254
272
  }
255
273
  }
256
- if (!hasStepUiSchema && !hasActionUiSchema) {
274
+ const selectOrSwitchMode = ["select", "switch"].includes((uiMode == null ? void 0 : uiMode.type) || uiMode);
275
+ if (!selectOrSwitchMode && !hasStepUiSchema && !hasActionUiSchema) {
257
276
  return null;
258
277
  }
259
278
  let mergedUiSchema = {};
260
279
  try {
261
280
  const resolvedSchema = await (0, import_utils.resolveStepUiSchema)(targetModel, flow, actionStep);
262
- if (!resolvedSchema) {
281
+ if (!resolvedSchema && !selectOrSwitchMode) {
263
282
  return null;
264
283
  }
265
284
  mergedUiSchema = resolvedSchema;
@@ -269,11 +288,12 @@ const DefaultSettingsIcon = /* @__PURE__ */ __name(({
269
288
  }
270
289
  return {
271
290
  stepKey,
272
- step: actionStep,
291
+ step,
273
292
  uiSchema: mergedUiSchema,
274
293
  title: t(stepTitle) || stepKey,
275
- modelKey
294
+ modelKey,
276
295
  // 添加模型标识
296
+ uiMode
277
297
  };
278
298
  })
279
299
  ).then((steps) => steps.filter(Boolean));
@@ -403,19 +423,55 @@ const DefaultSettingsIcon = /* @__PURE__ */ __name(({
403
423
  if (flattenSubMenus) {
404
424
  configurableFlowsAndSteps.forEach(({ flow, steps, modelKey }) => {
405
425
  const groupKey = generateUniqueKey(`flow-group-${modelKey ? `${modelKey}-` : ""}${flow.key}`);
406
- items.push({
407
- key: groupKey,
408
- label: t(flow.title) || flow.key,
409
- type: "group"
410
- });
426
+ if (flow.options.divider === "top") {
427
+ items.push({
428
+ type: "divider"
429
+ });
430
+ }
431
+ if (flow.options.enableTitle) {
432
+ items.push({
433
+ key: groupKey,
434
+ label: t(flow.title) || flow.key,
435
+ type: "group"
436
+ });
437
+ }
411
438
  steps.forEach((stepInfo) => {
412
439
  const baseMenuKey = modelKey ? `${modelKey}:${flow.key}:${stepInfo.stepKey}` : `${flow.key}:${stepInfo.stepKey}`;
413
440
  const uniqueKey = generateUniqueKey(baseMenuKey);
441
+ const uiMode = stepInfo.uiMode;
442
+ const subModel = findSubModelByKey(model, stepInfo.modelKey);
443
+ const targetModel = subModel || model;
444
+ const stepParams = targetModel.getStepParams(flow.key, stepInfo.stepKey) || {};
445
+ const itemProps = {
446
+ getDefaultValue: /* @__PURE__ */ __name(async () => {
447
+ var _a;
448
+ let defaultParams = await (0, import_utils.resolveDefaultParams)(stepInfo.step.defaultParams, targetModel.context);
449
+ if (stepInfo.step.use) {
450
+ const action = (_a = targetModel.getAction) == null ? void 0 : _a.call(targetModel, stepInfo.step.use);
451
+ defaultParams = await (0, import_utils.resolveDefaultParams)(action.defaultParams, targetModel.context);
452
+ }
453
+ return { ...defaultParams, ...stepParams };
454
+ }, "getDefaultValue"),
455
+ onChange: /* @__PURE__ */ __name(async (val) => {
456
+ var _a;
457
+ targetModel.setStepParams(flow.key, stepInfo.stepKey, val);
458
+ if (typeof stepInfo.step.beforeParamsSave === "function") {
459
+ await stepInfo.step.beforeParamsSave(targetModel.context, val, stepParams);
460
+ }
461
+ await targetModel.saveStepParams();
462
+ (_a = message == null ? void 0 : message.success) == null ? void 0 : _a.call(message, t("Configuration saved"));
463
+ if (typeof stepInfo.step.afterParamsSave === "function") {
464
+ await stepInfo.step.afterParamsSave(targetModel.context, val, stepParams);
465
+ }
466
+ }, "onChange"),
467
+ ...(uiMode == null ? void 0 : uiMode.props) || {},
468
+ itemKey: uiMode == null ? void 0 : uiMode.key
469
+ };
414
470
  items.push({
415
471
  key: uniqueKey,
416
- label: t(stepInfo.title)
472
+ label: /* @__PURE__ */ import_react.default.createElement(MenuLabelItem, { title: t(stepInfo.title), uiMode, itemProps })
417
473
  });
418
- if (flow.key === "popupSettings") {
474
+ if (flow.key === "popupSettings" && baseMenuKey.includes("openView")) {
419
475
  const copyKey = generateUniqueKey(`copy-pop-uid:${baseMenuKey}`);
420
476
  items.push({
421
477
  key: copyKey,
@@ -423,6 +479,11 @@ const DefaultSettingsIcon = /* @__PURE__ */ __name(({
423
479
  });
424
480
  }
425
481
  });
482
+ if (flow.options.divider === "bottom") {
483
+ items.push({
484
+ type: "divider"
485
+ });
486
+ }
426
487
  });
427
488
  } else {
428
489
  const modelGroups = /* @__PURE__ */ new Map();
@@ -494,9 +555,7 @@ const DefaultSettingsIcon = /* @__PURE__ */ __name(({
494
555
  const items = [...menuItems];
495
556
  if (showCopyUidButton || showDeleteButton) {
496
557
  items.push({
497
- key: "common-actions",
498
- label: t("Common actions"),
499
- type: "group"
558
+ type: "divider"
500
559
  });
501
560
  if (showCopyUidButton && model.uid) {
502
561
  items.push({
package/lib/flowEngine.js CHANGED
@@ -759,7 +759,11 @@ const _FlowEngine = class _FlowEngine {
759
759
  if (this.ensureModelRepository()) {
760
760
  await this._modelRepository.destroy(uid);
761
761
  }
762
- return this.removeModel(uid);
762
+ const modelInstance = this._modelInstances.get(uid);
763
+ const parent = modelInstance == null ? void 0 : modelInstance.parent;
764
+ const result = this.removeModel(uid);
765
+ parent && parent.emitter.emit("onSubModelDestroyed", modelInstance);
766
+ return result;
763
767
  }
764
768
  /**
765
769
  * Duplicate a model tree via repository API.
@@ -25,8 +25,8 @@ export interface FlowSettingsOpenOptions {
25
25
  /** 指定打开的步骤 key(配合 flowKey 使用) */
26
26
  stepKey?: string;
27
27
  /** 弹窗展现形式(drawer 或 dialog) */
28
- uiMode?: 'dialog' | 'drawer' | 'embed' | {
29
- type?: 'dialog' | 'drawer' | 'embed';
28
+ uiMode?: 'select' | 'switch' | 'dialog' | 'drawer' | 'embed' | {
29
+ type?: 'dialog' | 'drawer' | 'embed' | 'select' | 'switch';
30
30
  props?: {
31
31
  title?: string;
32
32
  width?: number;
@@ -466,12 +466,11 @@ const _FlowSettings = class _FlowSettings {
466
466
  if (!preset && (!step || await (0, import_utils.shouldHideStepInSettings)(model, flow, step))) continue;
467
467
  if (preset && !step.preset) continue;
468
468
  const mergedUiSchema = await (0, import_utils.resolveStepUiSchema)(model, flow, step);
469
- if (!mergedUiSchema || Object.keys(mergedUiSchema).length === 0) continue;
470
469
  let stepTitle = step.title;
471
470
  let beforeParamsSave = step.beforeParamsSave;
472
471
  let afterParamsSave = step.afterParamsSave;
473
472
  let actionDefaultParams = {};
474
- let uiMode2;
473
+ let uiMode2 = step.uiMode;
475
474
  if (step.use) {
476
475
  const action = (_b = model.getAction) == null ? void 0 : _b.call(model, step.use);
477
476
  if (action) {
@@ -497,6 +496,9 @@ const _FlowSettings = class _FlowSettings {
497
496
  ...resolvedDefaultParams || {},
498
497
  ...modelStepParams
499
498
  };
499
+ if ((!mergedUiSchema || Object.keys(mergedUiSchema).length === 0) && !["select", "switch"].includes((uiMode2 == null ? void 0 : uiMode2.type) || uiMode2)) {
500
+ continue;
501
+ }
500
502
  entries.push({
501
503
  flowKey: fk,
502
504
  flowTitle: t(flow.title) || fk,
@@ -522,6 +524,9 @@ const _FlowSettings = class _FlowSettings {
522
524
  const viewer = model.context.viewer;
523
525
  const resolvedUiMode = entries.length === 1 ? await (0, import_utils.resolveUiMode)(entries[0].uiMode || uiMode, entries[0].ctx) : uiMode;
524
526
  const modeType = typeof resolvedUiMode === "string" ? resolvedUiMode : resolvedUiMode.type || "dialog";
527
+ if (["select", "switch"].includes(modeType)) {
528
+ return;
529
+ }
525
530
  const openView = viewer[modeType || "dialog"].bind(viewer);
526
531
  const flowEngine = model.flowEngine;
527
532
  const scopes = {
@@ -265,10 +265,11 @@ const _SQLResource = class _SQLResource extends import_baseRecordResource.BaseRe
265
265
  try {
266
266
  this.clearError();
267
267
  this.loading = true;
268
+ this.emit("loading");
268
269
  const { data, meta } = await this.run();
269
270
  this.setData(data).setMeta(meta);
270
- this.emit("refresh");
271
271
  this.loading = false;
272
+ this.emit("refresh");
272
273
  resolve();
273
274
  } catch (error) {
274
275
  this.setError(error);
package/lib/types.d.ts CHANGED
@@ -94,6 +94,8 @@ export interface FlowDefinitionOptions<TModel extends FlowModel = FlowModel> {
94
94
  * 仅填补缺失,不覆盖已有。固定返回形状:{ [stepKey]: params }
95
95
  */
96
96
  defaultParams?: Record<string, any> | ((ctx: FlowModelContext) => StepParam | Promise<StepParam>);
97
+ enableTitle?: boolean;
98
+ divider?: 'top' | 'bottom';
97
99
  }
98
100
  export interface IModelComponentProps {
99
101
  [key: string]: any;
@@ -185,8 +187,9 @@ export interface DispatchEventOptions {
185
187
  */
186
188
  export type EventDefinition<TModel extends FlowModel = FlowModel, TCtx extends FlowContext = FlowContext> = ActionDefinition<TModel, TCtx>;
187
189
  export type StepUIMode = 'dialog' | 'drawer' | 'embed' | {
188
- type?: 'dialog' | 'drawer' | 'embed';
190
+ type?: 'dialog' | 'drawer' | 'embed' | 'select' | 'switch';
189
191
  props?: Record<string, any>;
192
+ key?: string;
190
193
  };
191
194
  /**
192
195
  * Step definition with unified support for both registered actions and inline handlers
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nocobase/flow-engine",
3
- "version": "2.0.0-alpha.53",
3
+ "version": "2.0.0-alpha.55",
4
4
  "private": false,
5
5
  "description": "A standalone flow engine for NocoBase, managing workflows, models, and actions.",
6
6
  "main": "lib/index.js",
@@ -8,8 +8,8 @@
8
8
  "dependencies": {
9
9
  "@formily/antd-v5": "1.x",
10
10
  "@formily/reactive": "2.x",
11
- "@nocobase/sdk": "2.0.0-alpha.53",
12
- "@nocobase/shared": "2.0.0-alpha.53",
11
+ "@nocobase/sdk": "2.0.0-alpha.55",
12
+ "@nocobase/shared": "2.0.0-alpha.55",
13
13
  "ahooks": "^3.7.2",
14
14
  "dayjs": "^1.11.9",
15
15
  "dompurify": "^3.0.2",
@@ -36,5 +36,5 @@
36
36
  ],
37
37
  "author": "NocoBase Team",
38
38
  "license": "AGPL-3.0",
39
- "gitHead": "ff30cf8261f5a97b3f24c3752af5bbc564167b37"
39
+ "gitHead": "7e65ad6b6e4e76a51f82c69b04b563fbcc7e1c25"
40
40
  }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
11
+ import { FlowEngine } from '../flowEngine';
12
+ import { FlowModel } from '../models';
13
+
14
+ describe('FlowEngine destroyModel', () => {
15
+ let engine: FlowEngine;
16
+
17
+ beforeEach(() => {
18
+ engine = new FlowEngine();
19
+ engine.registerModels({ FlowModel });
20
+ });
21
+
22
+ it('calls repository.destroy when repository exists and emits onSubModelDestroyed on parent', async () => {
23
+ const parent = engine.createModel({ uid: 'parent', use: 'FlowModel' });
24
+ const child = engine.createModel({
25
+ uid: 'child',
26
+ use: 'FlowModel',
27
+ parentId: 'parent',
28
+ subKey: 'child',
29
+ subType: 'object',
30
+ });
31
+
32
+ const repo = { destroy: vi.fn().mockResolvedValue(true) } as any;
33
+ (engine as any)._modelRepository = repo;
34
+
35
+ const emitSpy = vi.spyOn(parent.emitter, 'emit');
36
+
37
+ const res = await engine.destroyModel('child');
38
+
39
+ expect(repo.destroy).toHaveBeenCalledWith('child');
40
+ expect(res).toBe(true);
41
+ expect(emitSpy).toHaveBeenCalledWith('onSubModelDestroyed', child);
42
+ });
43
+
44
+ it('emits onSubModelDestroyed even if there is no repository', async () => {
45
+ const parent = engine.createModel({ uid: 'parent', use: 'FlowModel' });
46
+ const child = engine.createModel({
47
+ uid: 'child',
48
+ use: 'FlowModel',
49
+ parentId: 'parent',
50
+ subKey: 'child',
51
+ subType: 'object',
52
+ });
53
+
54
+ // ensure no repository
55
+ (engine as any)._modelRepository = null;
56
+
57
+ const emitSpy = vi.spyOn(parent.emitter, 'emit');
58
+
59
+ const res = await engine.destroyModel('child');
60
+
61
+ expect(res).toBe(true);
62
+ expect(emitSpy).toHaveBeenCalledWith('onSubModelDestroyed', child);
63
+ });
64
+
65
+ it('returns false when destroying a non-existent model but still calls repo.destroy if repo exists', async () => {
66
+ const repo = { destroy: vi.fn().mockResolvedValue(true) } as any;
67
+ (engine as any)._modelRepository = repo;
68
+
69
+ const res = await engine.destroyModel('no-such-uid');
70
+
71
+ expect(repo.destroy).toHaveBeenCalledWith('no-such-uid');
72
+ expect(res).toBe(false);
73
+ });
74
+ });
@@ -0,0 +1,108 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ import { Select } from 'antd';
11
+ import React, { useEffect, useRef, useState } from 'react';
12
+ import { useFlowEngineContext } from '../../../../provider';
13
+
14
+ export interface SelectWithTitleProps {
15
+ title?: any;
16
+ getDefaultValue?: any;
17
+ options?: any;
18
+ fieldNames?: any;
19
+ itemKey?: string;
20
+ onChange?: (...args: any[]) => void;
21
+ }
22
+
23
+ export function SelectWithTitle({
24
+ title,
25
+ getDefaultValue,
26
+ onChange,
27
+ options,
28
+ fieldNames,
29
+ itemKey,
30
+ ...others
31
+ }: SelectWithTitleProps) {
32
+ const [open, setOpen] = useState(false);
33
+ const [value, setValue] = useState<any>('');
34
+ const ctx = useFlowEngineContext();
35
+ useEffect(() => {
36
+ let cancelled = false;
37
+
38
+ const run = async () => {
39
+ if (!getDefaultValue) return;
40
+
41
+ try {
42
+ const val = await getDefaultValue();
43
+ if (cancelled || !val) return;
44
+
45
+ const entries = Object.entries(val);
46
+ if (!entries.length) return;
47
+
48
+ const [key, result] = entries[0];
49
+ setValue(result);
50
+ } catch (e) {
51
+ console.error(e);
52
+ }
53
+ };
54
+
55
+ run();
56
+
57
+ return () => {
58
+ cancelled = true;
59
+ };
60
+ }, [getDefaultValue]);
61
+
62
+ const timerRef = useRef<any>(null);
63
+
64
+ const handleChange = (val: any) => {
65
+ setValue(val);
66
+ onChange?.({ [itemKey]: val });
67
+ };
68
+ return (
69
+ <div
70
+ style={{ alignItems: 'center', display: 'flex', justifyContent: 'space-between' }}
71
+ onClick={(e) => {
72
+ e.stopPropagation();
73
+ setOpen((v) => !v);
74
+ }}
75
+ onMouseLeave={() => {
76
+ timerRef.current = setTimeout(() => {
77
+ setOpen(false);
78
+ }, 200);
79
+ }}
80
+ >
81
+ <span
82
+ style={{
83
+ whiteSpace: 'nowrap', // 不换行
84
+ flexShrink: 0, // 不被挤压
85
+ }}
86
+ >
87
+ {title}
88
+ </span>
89
+ <Select
90
+ {...others}
91
+ open={open}
92
+ popupMatchSelectWidth={false}
93
+ bordered={false}
94
+ value={value}
95
+ onChange={handleChange}
96
+ popupClassName={`select-popup-${title.replaceAll(' ', '-')}`}
97
+ fieldNames={fieldNames}
98
+ options={options}
99
+ labelRender={(props) => ctx.t(props.label)}
100
+ optionRender={(o) => ctx.t(o.label)}
101
+ style={{ textAlign: 'right', minWidth: 100 }}
102
+ onMouseEnter={() => {
103
+ clearTimeout(timerRef.current);
104
+ }}
105
+ />
106
+ </div>
107
+ );
108
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ import { observer } from '@formily/react';
11
+ import { Switch } from 'antd';
12
+ import React, { FC, useEffect, useState } from 'react';
13
+ import { useFlowEngineContext } from '../../../../provider';
14
+
15
+ const ml32 = { marginLeft: 32 };
16
+
17
+ export const SwitchWithTitle: FC = observer(
18
+ ({ title, onChange, getDefaultValue, disabled, itemKey, ...others }: any) => {
19
+ const [checked, setChecked] = useState<boolean>(false);
20
+ const ctx = useFlowEngineContext();
21
+
22
+ useEffect(() => {
23
+ let cancelled = false;
24
+
25
+ const run = async () => {
26
+ if (!getDefaultValue) return;
27
+
28
+ try {
29
+ const val = await getDefaultValue();
30
+ if (cancelled || !val) return;
31
+
32
+ const entries = Object.entries(val);
33
+ if (!entries.length) return;
34
+
35
+ const [key, value] = entries[0];
36
+ setChecked(!!value);
37
+ } catch (e) {
38
+ console.error(e);
39
+ }
40
+ };
41
+
42
+ run();
43
+
44
+ return () => {
45
+ cancelled = true;
46
+ };
47
+ }, [getDefaultValue]);
48
+
49
+ const handleChange = (val: boolean) => {
50
+ setChecked(val);
51
+ onChange?.({ [itemKey]: val });
52
+ };
53
+
54
+ // 点击整个容器时触发
55
+ const handleWrapperClick = () => {
56
+ if (disabled) return;
57
+ handleChange(!checked);
58
+ };
59
+ return (
60
+ <div
61
+ style={{
62
+ alignItems: 'center',
63
+ display: 'flex',
64
+ justifyContent: 'space-between',
65
+ cursor: disabled ? 'not-allowed' : 'pointer',
66
+ }}
67
+ onClick={handleWrapperClick}
68
+ >
69
+ {title}
70
+ <Switch
71
+ size="small"
72
+ {...others}
73
+ checkedChildren={others.checkedChildren ? ctx.t(others.checkedChildren) : undefined}
74
+ unCheckedChildren={others.unCheckedChildren ? ctx.t(others.unCheckedChildren) : undefined}
75
+ checked={checked}
76
+ style={ml32}
77
+ disabled={disabled}
78
+ />
79
+ </div>
80
+ );
81
+ },
82
+ );
@@ -10,13 +10,19 @@
10
10
  import { ExclamationCircleOutlined, MenuOutlined } from '@ant-design/icons';
11
11
  import type { DropdownProps, MenuProps } from 'antd';
12
12
  import { App, Dropdown, Modal } from 'antd';
13
- import React, { startTransition, useCallback, useEffect, useMemo, useState } from 'react';
13
+ import React, { startTransition, useCallback, useEffect, useMemo, useState, FC } from 'react';
14
14
  import { FlowModel } from '../../../../models';
15
- import { StepDefinition } from '../../../../types';
16
- import { getT, resolveStepUiSchema, shouldHideStepInSettings } from '../../../../utils';
17
- import { openStepSettings } from './StepSettings';
15
+ import { StepDefinition, StepUIMode } from '../../../../types';
16
+ import {
17
+ getT,
18
+ resolveStepUiSchema,
19
+ shouldHideStepInSettings,
20
+ resolveDefaultParams,
21
+ resolveUiMode,
22
+ } from '../../../../utils';
18
23
  import { useNiceDropdownMaxHeight } from '../../../../hooks';
19
-
24
+ import { SwitchWithTitle } from '../component/SwitchWithTitle';
25
+ import { SelectWithTitle } from '../component/SelectWithTitle';
20
26
  // Type definitions for better type safety
21
27
  interface StepInfo {
22
28
  stepKey: string;
@@ -24,6 +30,7 @@ interface StepInfo {
24
30
  uiSchema: Record<string, any>;
25
31
  title: string;
26
32
  modelKey?: string;
33
+ uiMode?: StepUIMode;
27
34
  }
28
35
 
29
36
  interface FlowInfo {
@@ -85,6 +92,22 @@ const findSubModelByKey = (model: FlowModel, subModelKey: string): FlowModel | n
85
92
  }
86
93
  };
87
94
 
95
+ const componentMap = {
96
+ switch: SwitchWithTitle,
97
+ select: SelectWithTitle,
98
+ };
99
+
100
+ const MenuLabelItem = ({ title, uiMode, itemProps }) => {
101
+ const type = uiMode?.type || uiMode;
102
+ const Component = type ? componentMap[type] : null;
103
+
104
+ if (!Component) {
105
+ return <>{title}</>;
106
+ }
107
+
108
+ return <Component title={title} {...itemProps} />;
109
+ };
110
+
88
111
  /**
89
112
  * 默认的设置菜单图标组件
90
113
  * 提供原有的配置菜单功能
@@ -281,12 +304,12 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
281
304
  const configurableSteps = await Promise.all(
282
305
  Object.entries(flow.steps).map(async ([stepKey, stepDefinition]) => {
283
306
  const actionStep = stepDefinition;
284
-
307
+ let step = actionStep;
285
308
  // 支持静态与动态 hideInSettings
286
309
  if (await shouldHideStepInSettings(targetModel, flow, actionStep)) {
287
310
  return null;
288
311
  }
289
-
312
+ let uiMode: any = await resolveUiMode(actionStep.uiMode, (targetModel as any).context);
290
313
  // 检查是否有uiSchema(静态或动态)
291
314
  const hasStepUiSchema = actionStep.uiSchema != null;
292
315
 
@@ -296,15 +319,18 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
296
319
  if (actionStep.use) {
297
320
  try {
298
321
  const action = targetModel.getAction?.(actionStep.use);
322
+ step = { ...(action || {}), ...actionStep };
323
+ uiMode = await resolveUiMode(action?.uiMode || uiMode, (targetModel as any).context);
299
324
  hasActionUiSchema = action && action.uiSchema != null;
300
325
  stepTitle = stepTitle || action?.title;
301
326
  } catch (error) {
302
327
  console.warn(t('Failed to get action {{action}}', { action: actionStep.use }), ':', error);
303
328
  }
304
329
  }
330
+ const selectOrSwitchMode = ['select', 'switch'].includes(uiMode?.type || uiMode);
305
331
 
306
332
  // 如果都没有uiSchema(静态或动态),返回null
307
- if (!hasStepUiSchema && !hasActionUiSchema) {
333
+ if (!selectOrSwitchMode && !hasStepUiSchema && !hasActionUiSchema) {
308
334
  return null;
309
335
  }
310
336
 
@@ -316,7 +342,7 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
316
342
  const resolvedSchema = await resolveStepUiSchema(targetModel, flow, actionStep);
317
343
 
318
344
  // 如果解析后没有可配置的UI Schema,跳过此步骤
319
- if (!resolvedSchema) {
345
+ if (!resolvedSchema && !selectOrSwitchMode) {
320
346
  return null;
321
347
  }
322
348
 
@@ -325,13 +351,13 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
325
351
  console.warn(t('Failed to resolve uiSchema for step {{stepKey}}', { stepKey }), ':', error);
326
352
  return null;
327
353
  }
328
-
329
354
  return {
330
355
  stepKey,
331
- step: actionStep,
356
+ step,
332
357
  uiSchema: mergedUiSchema,
333
358
  title: t(stepTitle) || stepKey,
334
359
  modelKey, // 添加模型标识
360
+ uiMode,
335
361
  };
336
362
  }),
337
363
  ).then((steps) => steps.filter(Boolean));
@@ -489,13 +515,19 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
489
515
  // 平铺模式:只有流程分组,没有模型层级
490
516
  configurableFlowsAndSteps.forEach(({ flow, steps, modelKey }: FlowInfo) => {
491
517
  const groupKey = generateUniqueKey(`flow-group-${modelKey ? `${modelKey}-` : ''}${flow.key}`);
492
-
518
+ if (flow.options.divider === 'top') {
519
+ items.push({
520
+ type: 'divider',
521
+ });
522
+ }
493
523
  // 在平铺模式下始终按流程分组
494
- items.push({
495
- key: groupKey,
496
- label: t(flow.title) || flow.key,
497
- type: 'group',
498
- });
524
+ if (flow.options.enableTitle) {
525
+ items.push({
526
+ key: groupKey,
527
+ label: t(flow.title) || flow.key,
528
+ type: 'group',
529
+ });
530
+ }
499
531
 
500
532
  steps.forEach((stepInfo: StepInfo) => {
501
533
  // 构建菜单项key,为子模型包含modelKey
@@ -504,14 +536,39 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
504
536
  : `${flow.key}:${stepInfo.stepKey}`;
505
537
 
506
538
  const uniqueKey = generateUniqueKey(baseMenuKey);
507
-
539
+ const uiMode = stepInfo.uiMode;
540
+ const subModel: any = findSubModelByKey(model, stepInfo.modelKey);
541
+ const targetModel = subModel || model;
542
+ const stepParams = targetModel.getStepParams(flow.key, stepInfo.stepKey) || {};
543
+ const itemProps = {
544
+ getDefaultValue: async () => {
545
+ let defaultParams = await resolveDefaultParams(stepInfo.step.defaultParams, targetModel.context);
546
+ if (stepInfo.step.use) {
547
+ const action = targetModel.getAction?.(stepInfo.step.use);
548
+ defaultParams = await resolveDefaultParams(action.defaultParams, targetModel.context);
549
+ }
550
+ return { ...defaultParams, ...stepParams };
551
+ },
552
+ onChange: async (val) => {
553
+ targetModel.setStepParams(flow.key, stepInfo.stepKey, val);
554
+ if (typeof stepInfo.step.beforeParamsSave === 'function') {
555
+ await stepInfo.step.beforeParamsSave(targetModel.context, val, stepParams);
556
+ }
557
+ await targetModel.saveStepParams();
558
+ message?.success?.(t('Configuration saved'));
559
+ if (typeof stepInfo.step.afterParamsSave === 'function') {
560
+ await stepInfo.step.afterParamsSave(targetModel.context, val, stepParams);
561
+ }
562
+ },
563
+ ...((uiMode as any)?.props || {}),
564
+ itemKey: (uiMode as any)?.key,
565
+ };
508
566
  items.push({
509
567
  key: uniqueKey,
510
- label: t(stepInfo.title),
568
+ label: <MenuLabelItem title={t(stepInfo.title)} uiMode={uiMode} itemProps={itemProps} />,
511
569
  });
512
-
513
570
  // add per-step copy popup uid under each configurable step
514
- if (flow.key === 'popupSettings') {
571
+ if (flow.key === 'popupSettings' && baseMenuKey.includes('openView')) {
515
572
  const copyKey = generateUniqueKey(`copy-pop-uid:${baseMenuKey}`);
516
573
  items.push({
517
574
  key: copyKey,
@@ -519,6 +576,11 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
519
576
  });
520
577
  }
521
578
  });
579
+ if (flow.options.divider === 'bottom') {
580
+ items.push({
581
+ type: 'divider',
582
+ });
583
+ }
522
584
  });
523
585
  } else {
524
586
  // 层级模式:真正的子菜单结构
@@ -542,7 +604,6 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
542
604
  // 直接添加当前模型的flows
543
605
  flows.forEach(({ flow, steps }: FlowInfo) => {
544
606
  const groupKey = generateUniqueKey(`flow-group-${flow.key}`);
545
-
546
607
  items.push({
547
608
  key: groupKey,
548
609
  label: t(flow.title) || flow.key,
@@ -608,12 +669,16 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
608
669
  const items = [...menuItems];
609
670
 
610
671
  if (showCopyUidButton || showDeleteButton) {
611
- // 使用分组呈现常用操作(不再使用分割线)
612
672
  items.push({
613
- key: 'common-actions',
614
- label: t('Common actions'),
615
- type: 'group' as const,
673
+ type: 'divider',
616
674
  });
675
+ // 使用分组呈现常用操作(不再使用分割线)
676
+
677
+ // items.push({
678
+ // key: 'common-actions',
679
+ // label: t('Common actions'),
680
+ // type: 'group' as const,
681
+ // });
617
682
 
618
683
  // 添加复制uid按钮
619
684
  if (showCopyUidButton && model.uid) {
@@ -49,9 +49,8 @@ const openStepSettings = async ({ model, flowKey, stepKey, width = 600, title }:
49
49
 
50
50
  // 解析 uiMode,支持函数式
51
51
  const resolvedUiMode = await resolveUiMode(step.uiMode, ctx);
52
-
53
52
  // 提取模式和属性
54
- let settingMode: 'dialog' | 'drawer' | 'embed';
53
+ let settingMode: 'dialog' | 'drawer' | 'embed' | 'select' | 'switch';
55
54
  let uiModeProps: Record<string, any> = {};
56
55
  let cleanup: () => void;
57
56
 
@@ -140,23 +140,19 @@ describe('DefaultSettingsIcon - only static flows are shown', () => {
140
140
  ),
141
141
  );
142
142
 
143
- // 等待菜单内出现静态流分组,确保异步加载完成
143
+ // 等待菜单渲染完成,并且只包含静态流的步骤
144
144
  await waitFor(() => {
145
145
  const menu = (globalThis as any).__lastDropdownMenu;
146
146
  expect(menu).toBeTruthy();
147
147
  const items = (menu?.items || []) as any[];
148
- const groupLabels = items.filter((it) => it.type === 'group').map((it) => String(it.label));
149
- expect(groupLabels).toContain('Static Flow');
148
+ const keys = items.map((it) => String(it.key || ''));
149
+ expect(keys.some((k) => k.startsWith('static1:'))).toBe(true);
150
+ expect(keys.some((k) => k.startsWith('dyn1:'))).toBe(false);
150
151
  });
151
152
 
152
153
  const menu = (globalThis as any).__lastDropdownMenu;
153
154
  const items = (menu?.items || []) as any[];
154
155
 
155
- // groups for flows are labeled with flow.title; ensure static group exists, dynamic group不存在
156
- const groupLabels = items.filter((it) => it.type === 'group').map((it) => String(it.label));
157
- expect(groupLabels).toContain('Static Flow');
158
- expect(groupLabels).not.toContain('Dynamic Flow');
159
-
160
156
  // 静态流的 step 存在(key: `${flowKey}:${stepKey}`),动态流 step 不存在
161
157
  expect(items.some((it) => String(it.key || '').startsWith('static1:'))).toBe(true);
162
158
  expect(items.some((it) => String(it.key || '').startsWith('dyn1:'))).toBe(false);
@@ -361,7 +357,7 @@ describe('DefaultSettingsIcon - only static flows are shown', () => {
361
357
  });
362
358
  });
363
359
 
364
- it('adds "Copy popup UID" for popupSettings flow (current model and sub-model)', async () => {
360
+ it('adds "Copy popup UID" for popupSettings openView step (current model and sub-model)', async () => {
365
361
  class Parent extends FlowModel {}
366
362
  class Child extends FlowModel {}
367
363
  const engine = new FlowEngine();
@@ -372,13 +368,13 @@ describe('DefaultSettingsIcon - only static flows are shown', () => {
372
368
  Parent.registerFlow({
373
369
  key: 'popupSettings',
374
370
  title: 'Popup',
375
- steps: { stage: { title: 'Stage', uiSchema: { a: { type: 'string', 'x-component': 'Input' } } } },
371
+ steps: { openView: { title: 'Open view', uiSchema: { a: { type: 'string', 'x-component': 'Input' } } } },
376
372
  });
377
373
  // sub model popupSettings
378
374
  Child.registerFlow({
379
375
  key: 'popupSettings',
380
376
  title: 'Popup Child',
381
- steps: { stage: { title: 'Stage', uiSchema: { a: { type: 'string', 'x-component': 'Input' } } } },
377
+ steps: { openView: { title: 'Open view', uiSchema: { a: { type: 'string', 'x-component': 'Input' } } } },
382
378
  });
383
379
  parent.addSubModel('items', child);
384
380
 
@@ -408,18 +404,18 @@ describe('DefaultSettingsIcon - only static flows are shown', () => {
408
404
  await waitFor(() => {
409
405
  const m = (globalThis as any).__lastDropdownMenu;
410
406
  const is = (m?.items || []) as any[];
411
- const current = is.find((it) => String(it.key) === 'copy-pop-uid:popupSettings:stage');
412
- const sub = is.find((it) => String(it.key).startsWith('copy-pop-uid:items[0]:popupSettings:stage'));
407
+ const current = is.find((it) => String(it.key) === 'copy-pop-uid:popupSettings:openView');
408
+ const sub = is.find((it) => String(it.key).startsWith('copy-pop-uid:items[0]:popupSettings:openView'));
413
409
  expect(current).toBeTruthy();
414
410
  expect(sub).toBeTruthy();
415
411
  });
416
412
 
417
413
  // click and verify clipboard(直接使用最新的 menu)
418
414
  const menu = (globalThis as any).__lastDropdownMenu;
419
- menu.onClick?.({ key: 'copy-pop-uid:popupSettings:stage' });
415
+ menu.onClick?.({ key: 'copy-pop-uid:popupSettings:openView' });
420
416
  expect((navigator as any).clipboard.writeText).toHaveBeenCalledWith('parent-2');
421
417
 
422
- menu.onClick?.({ key: 'copy-pop-uid:items[0]:popupSettings:stage' });
418
+ menu.onClick?.({ key: 'copy-pop-uid:items[0]:popupSettings:openView' });
423
419
  expect((navigator as any).clipboard.writeText).toHaveBeenCalledWith('child-2');
424
420
  });
425
421
 
package/src/flowEngine.ts CHANGED
@@ -902,7 +902,13 @@ export class FlowEngine {
902
902
  if (this.ensureModelRepository()) {
903
903
  await this._modelRepository.destroy(uid);
904
904
  }
905
- return this.removeModel(uid);
905
+
906
+ const modelInstance = this._modelInstances.get(uid) as FlowModel;
907
+ const parent = modelInstance?.parent;
908
+ const result = this.removeModel(uid);
909
+ parent && parent.emitter.emit('onSubModelDestroyed', modelInstance);
910
+
911
+ return result;
906
912
  }
907
913
 
908
914
  /**
@@ -53,11 +53,13 @@ export interface FlowSettingsOpenOptions {
53
53
  stepKey?: string;
54
54
  /** 弹窗展现形式(drawer 或 dialog) */
55
55
  uiMode?:
56
+ | 'select'
57
+ | 'switch'
56
58
  | 'dialog'
57
59
  | 'drawer'
58
60
  | 'embed'
59
61
  | {
60
- type?: 'dialog' | 'drawer' | 'embed';
62
+ type?: 'dialog' | 'drawer' | 'embed' | 'select' | 'switch';
61
63
  props?: {
62
64
  title?: string;
63
65
  width?: number;
@@ -597,14 +599,12 @@ export class FlowSettings {
597
599
 
598
600
  // 解析合并后的 uiSchema(包含 action 的 schema)
599
601
  const mergedUiSchema = await resolveStepUiSchema(model, flow, step);
600
- if (!mergedUiSchema || Object.keys(mergedUiSchema).length === 0) continue;
601
-
602
602
  // 计算标题与 hooks
603
603
  let stepTitle: string = step.title;
604
604
  let beforeParamsSave = step.beforeParamsSave;
605
605
  let afterParamsSave = step.afterParamsSave;
606
606
  let actionDefaultParams: Record<string, any> = {};
607
- let uiMode;
607
+ let uiMode = step.uiMode;
608
608
  if (step.use) {
609
609
  const action = model.getAction?.(step.use);
610
610
  if (action) {
@@ -633,7 +633,12 @@ export class FlowSettings {
633
633
  ...(resolvedDefaultParams || {}),
634
634
  ...modelStepParams,
635
635
  };
636
-
636
+ if (
637
+ (!mergedUiSchema || Object.keys(mergedUiSchema).length === 0) &&
638
+ !['select', 'switch'].includes(uiMode?.type || uiMode)
639
+ ) {
640
+ continue;
641
+ }
637
642
  entries.push({
638
643
  flowKey: fk,
639
644
  flowTitle: t(flow.title) || fk,
@@ -664,6 +669,9 @@ export class FlowSettings {
664
669
  const resolvedUiMode =
665
670
  entries.length === 1 ? await resolveUiMode(entries[0].uiMode || uiMode, entries[0].ctx) : uiMode;
666
671
  const modeType = typeof resolvedUiMode === 'string' ? resolvedUiMode : resolvedUiMode.type || 'dialog';
672
+ if (['select', 'switch'].includes(modeType)) {
673
+ return;
674
+ }
667
675
  const openView = viewer[modeType || 'dialog'].bind(viewer);
668
676
  const flowEngine = (model as any).flowEngine as FlowEngine;
669
677
  const scopes = {
@@ -276,10 +276,11 @@ export class SQLResource<TData = any> extends BaseRecordResource<TData> {
276
276
  try {
277
277
  this.clearError();
278
278
  this.loading = true;
279
+ this.emit('loading');
279
280
  const { data, meta } = await this.run();
280
281
  this.setData(data).setMeta(meta);
281
- this.emit('refresh');
282
282
  this.loading = false;
283
+ this.emit('refresh');
283
284
  resolve();
284
285
  } catch (error) {
285
286
  this.setError(error);
package/src/types.ts CHANGED
@@ -110,6 +110,8 @@ export interface FlowDefinitionOptions<TModel extends FlowModel = FlowModel> {
110
110
  * 仅填补缺失,不覆盖已有。固定返回形状:{ [stepKey]: params }
111
111
  */
112
112
  defaultParams?: Record<string, any> | ((ctx: FlowModelContext) => StepParam | Promise<StepParam>);
113
+ enableTitle?: boolean;
114
+ divider?: 'top' | 'bottom';
113
115
  }
114
116
 
115
117
  export interface IModelComponentProps {
@@ -242,7 +244,7 @@ export type StepUIMode =
242
244
  | 'embed'
243
245
  // | 'switch'
244
246
  // | 'select'
245
- | { type?: 'dialog' | 'drawer' | 'embed'; props?: Record<string, any> };
247
+ | { type?: 'dialog' | 'drawer' | 'embed' | 'select' | 'switch'; props?: Record<string, any>; key?: string };
246
248
  // | { type: 'switch'; props?: Record<string, any> }
247
249
  // | { type: 'select'; props?: Record<string, any> }
248
250