@nocobase/flow-engine 2.0.30 → 2.0.32

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.
@@ -36,7 +36,6 @@ const APP_CONTAINER_SELECTOR = "#nocobase-app-container";
36
36
  const DRAWER_CONTENT_WRAPPER_SELECTOR = ".ant-drawer-content-wrapper";
37
37
  const DRAWER_CONTENT_SELECTOR = ".ant-drawer-content";
38
38
  const DRAWER_ROOT_SELECTOR = ".ant-drawer-root";
39
- const MODAL_CONTENT_SELECTOR = ".ant-modal-content";
40
39
  const MODAL_SELECTOR = ".ant-modal";
41
40
  const MODAL_WRAP_SELECTOR = ".ant-modal-wrap";
42
41
  const MODAL_ROOT_SELECTOR = ".ant-modal-root";
@@ -54,9 +53,8 @@ const createAbsolutePortalHostConfig = /* @__PURE__ */ __name((element) => ({
54
53
  }), "createAbsolutePortalHostConfig");
55
54
  const popupPortalHostResolvers = [
56
55
  (hostEl) => getClosestElement(hostEl, DRAWER_CONTENT_WRAPPER_SELECTOR),
57
- (hostEl) => getClosestElement(hostEl, MODAL_CONTENT_SELECTOR),
58
- (hostEl) => getClosestElement(hostEl, MODAL_SELECTOR),
59
56
  (hostEl) => getClosestElement(hostEl, MODAL_WRAP_SELECTOR),
57
+ (hostEl) => getClosestElement(hostEl, MODAL_SELECTOR),
60
58
  (hostEl) => {
61
59
  const drawerContent = getClosestElement(hostEl, DRAWER_CONTENT_SELECTOR);
62
60
  return drawerContent ? getClosestElement(drawerContent, DRAWER_CONTENT_WRAPPER_SELECTOR) || drawerContent : null;
@@ -57,6 +57,7 @@ __export(flowContext_exports, {
57
57
  });
58
58
  module.exports = __toCommonJS(flowContext_exports);
59
59
  var import_reactive = require("@formily/reactive");
60
+ var import_axios = __toESM(require("axios"));
60
61
  var antd = __toESM(require("antd"));
61
62
  var import_lodash = __toESM(require("lodash"));
62
63
  var import_qs = __toESM(require("qs"));
@@ -81,6 +82,28 @@ var import_dayjs = __toESM(require("dayjs"));
81
82
  var import_runjsLibs = require("./runjsLibs");
82
83
  var import_runjsModuleLoader = require("./utils/runjsModuleLoader");
83
84
  var _proxy, _FlowContext_instances, createChildNodes_fn, findMetaByPath_fn, findMetaInDelegatesDeep_fn, findMetaInProperty_fn, resolvePathInMeta_fn, resolvePathInMetaAsync_fn, buildParentTitles_fn, toTreeNode_fn;
85
+ function normalizePathname(pathname) {
86
+ return pathname.endsWith("/") ? pathname : `${pathname}/`;
87
+ }
88
+ __name(normalizePathname, "normalizePathname");
89
+ function shouldBypassApiClient(url, app) {
90
+ try {
91
+ const requestUrl = new URL(url);
92
+ if (!["http:", "https:"].includes(requestUrl.protocol)) {
93
+ return false;
94
+ }
95
+ if (!(app == null ? void 0 : app.getApiUrl)) {
96
+ return true;
97
+ }
98
+ const apiUrl = new URL(app.getApiUrl());
99
+ const apiPath = normalizePathname(apiUrl.pathname);
100
+ const requestPath = normalizePathname(requestUrl.pathname);
101
+ return requestUrl.origin !== apiUrl.origin || !requestPath.startsWith(apiPath);
102
+ } catch {
103
+ return false;
104
+ }
105
+ }
106
+ __name(shouldBypassApiClient, "shouldBypassApiClient");
84
107
  function isRecordRefLike(val) {
85
108
  return !!(val && typeof val === "object" && "collection" in val && "filterByTk" in val);
86
109
  }
@@ -2211,6 +2234,10 @@ const _BaseFlowEngineContext = class _BaseFlowEngineContext extends FlowContext
2211
2234
  return this.engine.getModel(modelName, searchInPreviousEngines);
2212
2235
  });
2213
2236
  this.defineMethod("request", (options) => {
2237
+ const app = this.app;
2238
+ if (typeof (options == null ? void 0 : options.url) === "string" && shouldBypassApiClient(options.url, app)) {
2239
+ return import_axios.default.request(options);
2240
+ }
2214
2241
  return this.api.request(options);
2215
2242
  });
2216
2243
  this.defineMethod(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nocobase/flow-engine",
3
- "version": "2.0.30",
3
+ "version": "2.0.32",
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,9 +8,10 @@
8
8
  "dependencies": {
9
9
  "@formily/antd-v5": "1.x",
10
10
  "@formily/reactive": "2.x",
11
- "@nocobase/sdk": "2.0.30",
12
- "@nocobase/shared": "2.0.30",
11
+ "@nocobase/sdk": "2.0.32",
12
+ "@nocobase/shared": "2.0.32",
13
13
  "ahooks": "^3.7.2",
14
+ "axios": "^1.7.0",
14
15
  "dayjs": "^1.11.9",
15
16
  "dompurify": "^3.0.2",
16
17
  "lodash": "^4.x",
@@ -36,5 +37,5 @@
36
37
  ],
37
38
  "author": "NocoBase Team",
38
39
  "license": "Apache-2.0",
39
- "gitHead": "6ac70f8dda4e7970242435bc1d1a5b70954e643f"
40
+ "gitHead": "ad454bc0bc27dee7e931644200317d034c816e3e"
40
41
  }
@@ -7,7 +7,8 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import { describe, expect, it, vi } from 'vitest';
10
+ import axios from 'axios';
11
+ import { describe, expect, it, vi, afterEach } from 'vitest';
11
12
  import { FlowContext, FlowRuntimeContext, FlowRunJSContext, type PropertyMetaFactory } from '../flowContext';
12
13
  import { FlowEngine } from '../flowEngine';
13
14
  import { FlowModel } from '../models/flowModel';
@@ -1630,6 +1631,69 @@ describe('runAction delegation from runtime context', () => {
1630
1631
  });
1631
1632
  });
1632
1633
 
1634
+ describe('FlowContext request defaults', () => {
1635
+ class RequestModel extends FlowModel {}
1636
+
1637
+ afterEach(() => {
1638
+ vi.restoreAllMocks();
1639
+ });
1640
+
1641
+ const createRequestContext = () => {
1642
+ const engine = new FlowEngine();
1643
+ engine.registerModels({ RequestModel });
1644
+
1645
+ const apiRequest = vi.fn(async (options) => options);
1646
+ const app = {
1647
+ getApiUrl(pathname = '') {
1648
+ return 'https://app.example.com/api/'.replace(/\/$/g, '') + '/' + pathname.replace(/^\//g, '');
1649
+ },
1650
+ };
1651
+
1652
+ engine.context.defineProperty('api', { value: { request: apiRequest } as any });
1653
+ engine.context.defineProperty('app', { value: app });
1654
+
1655
+ const model = engine.createModel({ use: 'RequestModel' });
1656
+ const ctx = new FlowRuntimeContext(model, 'flow');
1657
+ const directAxiosRequest = vi.spyOn(axios, 'request').mockResolvedValue({ data: {} } as any);
1658
+
1659
+ return { ctx, apiRequest, directAxiosRequest };
1660
+ };
1661
+
1662
+ it.each([
1663
+ ['apiClient', 'users:list', 'api'],
1664
+ ['apiClient', '/api/users:list', 'api'],
1665
+ ['apiClient', 'https://app.example.com/api/users:list', 'api'],
1666
+ ['direct axios', 'https://app.example.com/custom-api/users', 'axios'],
1667
+ ])('should use %s for %s', async (_target, url, expected) => {
1668
+ const { ctx, apiRequest, directAxiosRequest } = createRequestContext();
1669
+
1670
+ await ctx.request({ url, method: 'get' });
1671
+
1672
+ if (expected === 'api') {
1673
+ expect(apiRequest).toHaveBeenCalledTimes(1);
1674
+ expect(directAxiosRequest).not.toHaveBeenCalled();
1675
+ return;
1676
+ }
1677
+
1678
+ expect(directAxiosRequest).toHaveBeenCalledTimes(1);
1679
+ expect(apiRequest).not.toHaveBeenCalled();
1680
+ });
1681
+
1682
+ it('should use direct axios for cross-origin absolute urls', async () => {
1683
+ const { ctx, apiRequest, directAxiosRequest } = createRequestContext();
1684
+
1685
+ await ctx.request({ url: 'https://api.example.com/users', method: 'get', skipAuth: false });
1686
+
1687
+ expect(directAxiosRequest).toHaveBeenCalledTimes(1);
1688
+ expect(apiRequest).not.toHaveBeenCalled();
1689
+ expect(directAxiosRequest.mock.calls[0][0]).toMatchObject({
1690
+ url: 'https://api.example.com/users',
1691
+ method: 'get',
1692
+ skipAuth: false,
1693
+ });
1694
+ });
1695
+ });
1696
+
1633
1697
  describe('FlowContext delayed meta loading', () => {
1634
1698
  // 测试场景:属性定义时 meta 为异步函数,首次访问时延迟加载
1635
1699
  // 输入:属性带有异步 meta 函数
@@ -242,7 +242,8 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
242
242
  }, [onDropdownVisibleChange]);
243
243
  const resolvePopupContainer = useCallback<NonNullable<DropdownProps['getPopupContainer']>>(
244
244
  (triggerNode) => {
245
- // 优先挂到工具栏自身容器,避免 modal / drawer 中鼠标从图标移动到菜单时先离开工具栏树。
245
+ // 工具栏自身容器必须优先,保证鼠标从 icon 移到菜单时仍处于同一 hover 树。
246
+ // 弹窗场景的裁剪问题由 useFloatToolbarPortal 负责把 toolbar 挂到正确的 popup host。
246
247
  return (
247
248
  getToolbarPopupContainer(triggerNode) ||
248
249
  getPopupContainer?.(triggerNode) ||
@@ -14,7 +14,6 @@ const APP_CONTAINER_SELECTOR = '#nocobase-app-container';
14
14
  const DRAWER_CONTENT_WRAPPER_SELECTOR = '.ant-drawer-content-wrapper';
15
15
  const DRAWER_CONTENT_SELECTOR = '.ant-drawer-content';
16
16
  const DRAWER_ROOT_SELECTOR = '.ant-drawer-root';
17
- const MODAL_CONTENT_SELECTOR = '.ant-modal-content';
18
17
  const MODAL_SELECTOR = '.ant-modal';
19
18
  const MODAL_WRAP_SELECTOR = '.ant-modal-wrap';
20
19
  const MODAL_ROOT_SELECTOR = '.ant-modal-root';
@@ -79,9 +78,8 @@ const createAbsolutePortalHostConfig = (element: HTMLElement): ToolbarPortalHost
79
78
 
80
79
  const popupPortalHostResolvers: Array<(hostEl: HTMLElement | null) => HTMLElement | null> = [
81
80
  (hostEl) => getClosestElement(hostEl, DRAWER_CONTENT_WRAPPER_SELECTOR),
82
- (hostEl) => getClosestElement(hostEl, MODAL_CONTENT_SELECTOR),
83
- (hostEl) => getClosestElement(hostEl, MODAL_SELECTOR),
84
81
  (hostEl) => getClosestElement(hostEl, MODAL_WRAP_SELECTOR),
82
+ (hostEl) => getClosestElement(hostEl, MODAL_SELECTOR),
85
83
  (hostEl) => {
86
84
  const drawerContent = getClosestElement(hostEl, DRAWER_CONTENT_SELECTOR);
87
85
  return drawerContent ? getClosestElement(drawerContent, DRAWER_CONTENT_WRAPPER_SELECTOR) || drawerContent : null;
@@ -11,6 +11,7 @@ import { ISchema } from '@formily/json-schema';
11
11
  import { observable } from '@formily/reactive';
12
12
  import { APIClient, RequestOptions } from '@nocobase/sdk';
13
13
  import type { Router } from '@remix-run/router';
14
+ import axios from 'axios';
14
15
  import { MessageInstance } from 'antd/es/message/interface';
15
16
  import * as antd from 'antd';
16
17
  import type { HookAPI } from 'antd/es/modal/useModal';
@@ -58,6 +59,31 @@ import dayjs from 'dayjs';
58
59
  import { externalReactRender, setupRunJSLibs } from './runjsLibs';
59
60
  import { runjsImportAsync, runjsImportModule, runjsRequireAsync } from './utils/runjsModuleLoader';
60
61
 
62
+ function normalizePathname(pathname: string) {
63
+ return pathname.endsWith('/') ? pathname : `${pathname}/`;
64
+ }
65
+
66
+ function shouldBypassApiClient(url: string, app?: { getApiUrl?: (pathname?: string) => string }) {
67
+ try {
68
+ const requestUrl = new URL(url);
69
+ if (!['http:', 'https:'].includes(requestUrl.protocol)) {
70
+ return false;
71
+ }
72
+
73
+ if (!app?.getApiUrl) {
74
+ return true;
75
+ }
76
+
77
+ const apiUrl = new URL(app.getApiUrl());
78
+ const apiPath = normalizePathname(apiUrl.pathname);
79
+ const requestPath = normalizePathname(requestUrl.pathname);
80
+
81
+ return requestUrl.origin !== apiUrl.origin || !requestPath.startsWith(apiPath);
82
+ } catch {
83
+ return false;
84
+ }
85
+ }
86
+
61
87
  // Helper: detect a RecordRef-like object
62
88
  function isRecordRefLike(val: any): boolean {
63
89
  return !!(val && typeof val === 'object' && 'collection' in val && 'filterByTk' in val);
@@ -3024,6 +3050,10 @@ class BaseFlowEngineContext extends FlowContext {
3024
3050
  return this.engine.getModel(modelName, searchInPreviousEngines);
3025
3051
  });
3026
3052
  this.defineMethod('request', (options: RequestOptions) => {
3053
+ const app = this.app as { getApiUrl?: (pathname?: string) => string } | undefined;
3054
+ if (typeof options?.url === 'string' && shouldBypassApiClient(options.url, app)) {
3055
+ return axios.request(options);
3056
+ }
3027
3057
  return this.api.request(options);
3028
3058
  });
3029
3059
  this.defineMethod(