@nocobase/flow-engine 2.0.0-alpha.39 → 2.0.0-alpha.40

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.
package/lib/acl/Acl.d.ts CHANGED
@@ -18,6 +18,7 @@ export declare class ACL {
18
18
  private dataSources;
19
19
  private loaded;
20
20
  private loadingPromise;
21
+ private lastToken;
21
22
  constructor(flowEngine: FlowEngine);
22
23
  load(): Promise<void>;
23
24
  getActionAlias(actionName: string): any;
package/lib/acl/Acl.js CHANGED
@@ -37,7 +37,17 @@ const _ACL = class _ACL {
37
37
  dataSources = {};
38
38
  loaded = false;
39
39
  loadingPromise = null;
40
+ // 用于识别当前鉴权状态(例如切换登录用户后应重新加载)
41
+ // 记录上一次用于鉴权的 token,用于识别登录态变更
42
+ lastToken = null;
40
43
  async load() {
44
+ var _a, _b, _c, _d;
45
+ const currentToken = ((_d = (_c = (_b = (_a = this.flowEngine) == null ? void 0 : _a.context) == null ? void 0 : _b.api) == null ? void 0 : _c.auth) == null ? void 0 : _d.token) || "";
46
+ if (this.loaded && this.lastToken !== currentToken) {
47
+ this.loaded = false;
48
+ this.loadingPromise = null;
49
+ this.dataSources = {};
50
+ }
41
51
  if (this.loaded) return;
42
52
  if (!this.loadingPromise) {
43
53
  this.loadingPromise = (async () => {
@@ -46,6 +56,7 @@ const _ACL = class _ACL {
46
56
  });
47
57
  this.dataSources = data || {};
48
58
  this.loaded = true;
59
+ this.lastToken = currentToken;
49
60
  })();
50
61
  }
51
62
  await this.loadingPromise;
@@ -78,9 +78,11 @@ function FieldModelRenderer(props) {
78
78
  const handleCompositionStart = /* @__PURE__ */ __name(() => {
79
79
  composingRef.current = true;
80
80
  }, "handleCompositionStart");
81
- const handleCompositionEnd = /* @__PURE__ */ __name((e) => {
81
+ const handleCompositionEnd = /* @__PURE__ */ __name((e, flag = true) => {
82
82
  composingRef.current = false;
83
- props.onChange(e);
83
+ if (flag) {
84
+ props.onChange(e);
85
+ }
84
86
  }, "handleCompositionEnd");
85
87
  const modelProps = {
86
88
  ...import_lodash.default.omit(rest, flowModelRendererPropKeys),
@@ -83,6 +83,7 @@ const renderToolbarItems = /* @__PURE__ */ __name((model, showDeleteButton, show
83
83
  return /* @__PURE__ */ import_react.default.createElement(ItemComponent, { key: itemConfig.key, model });
84
84
  });
85
85
  }, "renderToolbarItems");
86
+ const TOOLBAR_ITEM_WIDTH = 19;
86
87
  const toolbarPositionToCSS = {
87
88
  inside: `
88
89
  top: 2px;
@@ -90,7 +91,7 @@ const toolbarPositionToCSS = {
90
91
  above: `
91
92
  top: 0px;
92
93
  transform: translateY(-100%);
93
- padding-bottom: 2px;
94
+ padding-bottom: 0px;
94
95
  margin-bottom: -2px;
95
96
  `,
96
97
  below: `
@@ -100,7 +101,7 @@ const toolbarPositionToCSS = {
100
101
  margin-top: -2px;
101
102
  `
102
103
  };
103
- const floatContainerStyles = /* @__PURE__ */ __name(({ showBackground, showBorder, ctx, toolbarPosition = "inside" }) => import_css.css`
104
+ const floatContainerStyles = /* @__PURE__ */ __name(({ showBackground, showBorder, ctx, toolbarPosition = "inside", toolbarCount }) => import_css.css`
104
105
  position: relative;
105
106
  display: inline;
106
107
 
@@ -136,6 +137,7 @@ const floatContainerStyles = /* @__PURE__ */ __name(({ showBackground, showBorde
136
137
  border: ${showBorder ? "2px solid var(--colorBorderSettingsHover)" : ""};
137
138
  border-radius: ${ctx.themeToken.borderRadiusLG}px;
138
139
  pointer-events: none;
140
+ min-width: ${TOOLBAR_ITEM_WIDTH * toolbarCount}px;
139
141
 
140
142
  &.nb-in-template {
141
143
  background: var(--colorTemplateBgSettingsHover);
@@ -431,7 +433,13 @@ const FlowsFloatContextMenuWithModel = (0, import_react2.observer)(
431
433
  "div",
432
434
  {
433
435
  ref: containerRef,
434
- className: `${floatContainerStyles({ showBackground, showBorder, ctx: model.context, toolbarPosition })} ${hideMenu ? "hide-parent-menu" : ""} ${hasButton ? "has-button-child" : ""} ${className || ""}`,
436
+ className: `${floatContainerStyles({
437
+ showBackground,
438
+ showBorder,
439
+ ctx: model.context,
440
+ toolbarPosition,
441
+ toolbarCount: getToolbarCount(flowEngine, extraToolbarItems)
442
+ })} ${hideMenu ? "hide-parent-menu" : ""} ${hasButton ? "has-button-child" : ""} ${className || ""}`,
435
443
  style: containerStyle,
436
444
  "data-has-float-menu": "true",
437
445
  onMouseMove: handleChildHover
@@ -492,6 +500,13 @@ const FlowsFloatContextMenuWithModelById = (0, import_react2.observer)(
492
500
  displayName: "FlowsFloatContextMenuWithModelById"
493
501
  }
494
502
  );
503
+ function getToolbarCount(flowEngine, extraToolbarItems) {
504
+ var _a, _b;
505
+ const toolbarItems = ((_b = (_a = flowEngine == null ? void 0 : flowEngine.flowSettings) == null ? void 0 : _a.getToolbarItems) == null ? void 0 : _b.call(_a)) || [];
506
+ const allToolbarItems = [...toolbarItems, ...extraToolbarItems || []];
507
+ return allToolbarItems.length;
508
+ }
509
+ __name(getToolbarCount, "getToolbarCount");
495
510
  // Annotate the CommonJS export names for ESM import in node:
496
511
  0 && (module.exports = {
497
512
  FlowsFloatContextMenu
@@ -40,6 +40,7 @@ export declare class CollectionFieldModel<T extends DefaultStructure = DefaultSt
40
40
  static getDefaultBindingByField(ctx: FlowEngineContext, collectionField: CollectionField, options?: {
41
41
  useStrict?: boolean;
42
42
  fallbackToTargetTitleField?: boolean;
43
+ targetCollectionTitleField?: CollectionField;
43
44
  }): BindingOptions | null;
44
45
  static bindModelToInterface(modelName: string, interfaceName: string | string[], options?: {
45
46
  isDefault?: boolean;
@@ -159,8 +159,11 @@ const _CollectionFieldModel = class _CollectionFieldModel extends import_flowMod
159
159
  if (options.fallbackToTargetTitleField) {
160
160
  const binding = this.getDefaultBindingByField(ctx, collectionField, { useStrict: true });
161
161
  if (!binding) {
162
- if (collectionField.isAssociationField() && collectionField.targetCollectionTitleField) {
163
- return this.getDefaultBindingByField(ctx, collectionField.targetCollectionTitleField);
162
+ if (collectionField.isAssociationField() && (options == null ? void 0 : options.targetCollectionTitleField) || collectionField.targetCollectionTitleField) {
163
+ return this.getDefaultBindingByField(
164
+ ctx,
165
+ (options == null ? void 0 : options.targetCollectionTitleField) || collectionField.targetCollectionTitleField
166
+ );
164
167
  }
165
168
  }
166
169
  return binding;
@@ -146,21 +146,22 @@ const _MultiRecordResource = class _MultiRecordResource extends import_baseRecor
146
146
  }
147
147
  async update(filterByTk, data, options) {
148
148
  const collection = this.context.collection;
149
- const filterTargetKey = collection.filterTargetKey;
150
- let result = data;
149
+ const filterTargetKey = collection == null ? void 0 : collection.filterTargetKey;
151
150
  const tkData = collection == null ? void 0 : collection.getFilterByTK(this.context.record);
152
- if (Array.isArray(filterTargetKey)) {
153
- result = {
154
- ...data,
155
- ...tkData || {}
156
- };
157
- } else {
158
- result = {
159
- ...data,
160
- [filterTargetKey]: tkData
161
- };
151
+ let result = data;
152
+ if (collection && filterTargetKey) {
153
+ if (Array.isArray(filterTargetKey)) {
154
+ result = {
155
+ ...data,
156
+ ...tkData || {}
157
+ };
158
+ } else {
159
+ result = {
160
+ ...data,
161
+ [filterTargetKey]: tkData
162
+ };
163
+ }
162
164
  }
163
- console.log(result);
164
165
  const config = this.mergeRequestConfig(
165
166
  {
166
167
  params: {
@@ -66,18 +66,20 @@ const _SingleRecordResource = class _SingleRecordResource extends import_baseRec
66
66
  config.params.filterByTk = this.getFilterByTk();
67
67
  actionName = "update";
68
68
  const collection = this.context.collection;
69
- const filterTargetKey = collection.filterTargetKey;
69
+ const filterTargetKey = collection == null ? void 0 : collection.filterTargetKey;
70
70
  const tkData = collection == null ? void 0 : collection.getFilterByTK(this.context.record);
71
- if (Array.isArray(filterTargetKey)) {
72
- result = {
73
- ...data,
74
- ...tkData || {}
75
- };
76
- } else {
77
- result = {
78
- ...data,
79
- [filterTargetKey]: tkData
80
- };
71
+ if (collection && filterTargetKey) {
72
+ if (Array.isArray(filterTargetKey)) {
73
+ result = {
74
+ ...data,
75
+ ...tkData || {}
76
+ };
77
+ } else {
78
+ result = {
79
+ ...data,
80
+ [filterTargetKey]: tkData
81
+ };
82
+ }
81
83
  }
82
84
  }
83
85
  await this.runAction(actionName, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nocobase/flow-engine",
3
- "version": "2.0.0-alpha.39",
3
+ "version": "2.0.0-alpha.40",
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.39",
12
- "@nocobase/shared": "2.0.0-alpha.39",
11
+ "@nocobase/sdk": "2.0.0-alpha.40",
12
+ "@nocobase/shared": "2.0.0-alpha.40",
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": "f716c78b4389d45ebe8996bcabcca976b4410aa2"
39
+ "gitHead": "dfb9fd33f43e91e676ae7b1f1a595bc38b7c20d2"
40
40
  }
package/src/acl/Acl.tsx CHANGED
@@ -21,10 +21,23 @@ export class ACL {
21
21
  private dataSources: Record<string, any> = {};
22
22
  private loaded = false;
23
23
  private loadingPromise: Promise<void> | null = null;
24
+ // 用于识别当前鉴权状态(例如切换登录用户后应重新加载)
25
+ // 记录上一次用于鉴权的 token,用于识别登录态变更
26
+ private lastToken: string | null = null;
24
27
 
25
28
  constructor(private flowEngine: FlowEngine) {}
26
29
 
27
30
  async load() {
31
+ // 基于 token 识别登录态是否发生变化
32
+ const currentToken = this.flowEngine?.context?.api?.auth?.token || '';
33
+
34
+ // 已加载但登录态变更:强制重载
35
+ if (this.loaded && this.lastToken !== currentToken) {
36
+ this.loaded = false;
37
+ this.loadingPromise = null;
38
+ this.dataSources = {};
39
+ }
40
+
28
41
  if (this.loaded) return;
29
42
 
30
43
  if (!this.loadingPromise) {
@@ -34,6 +47,7 @@ export class ACL {
34
47
  });
35
48
  this.dataSources = data || {};
36
49
  this.loaded = true;
50
+ this.lastToken = currentToken;
37
51
  })();
38
52
  }
39
53
 
@@ -69,4 +69,47 @@ describe('ACL', () => {
69
69
  });
70
70
  expect(notOk).toBe(false);
71
71
  });
72
+
73
+ it('reloads permissions when auth token changes', async () => {
74
+ const payload1 = {
75
+ data: {
76
+ allowAll: false,
77
+ actionAlias: { remove: 'destroy' },
78
+ resources: ['posts'],
79
+ actions: {},
80
+ strategy: { actions: [] },
81
+ },
82
+ };
83
+ const payload2 = {
84
+ data: {
85
+ allowAll: false,
86
+ actionAlias: { remove: 'erase' },
87
+ resources: ['posts'],
88
+ actions: {},
89
+ strategy: { actions: [] },
90
+ },
91
+ };
92
+
93
+ const engine = new FlowEngine();
94
+ const api: any = {
95
+ auth: { token: 't1' },
96
+ request: vi.fn().mockImplementation(async () => {
97
+ // 返回依据当前 token 的不同 ACL 数据
98
+ return api.auth.token === 't1' ? { data: payload1 } : { data: payload2 };
99
+ }),
100
+ };
101
+ engine.context.defineProperty('api', { value: api });
102
+
103
+ const acl = new ACL(engine);
104
+ await acl.load();
105
+ expect(acl.getActionAlias('remove')).toBe('destroy');
106
+
107
+ // 切换 token,应触发下次校验时的 ACL 重载
108
+ api.auth.token = 't2';
109
+ await acl.aclCheck({ dataSourceKey: 'main', resourceName: 'posts', actionName: 'remove' });
110
+ expect(acl.getActionAlias('remove')).toBe('erase');
111
+
112
+ // 确认 roles:check 请求至少调用两次(初次 + 重载)
113
+ expect(api.request).toHaveBeenCalledTimes(2);
114
+ });
72
115
  });
@@ -54,9 +54,11 @@ export function FieldModelRenderer(props: any) {
54
54
  composingRef.current = true;
55
55
  };
56
56
 
57
- const handleCompositionEnd = (e: React.CompositionEvent<HTMLInputElement>) => {
57
+ const handleCompositionEnd = (e: React.CompositionEvent<HTMLInputElement>, flag = true) => {
58
58
  composingRef.current = false;
59
- props.onChange(e);
59
+ if (flag) {
60
+ props.onChange(e);
61
+ }
60
62
  };
61
63
 
62
64
  const modelProps = {
@@ -80,6 +80,9 @@ const renderToolbarItems = (
80
80
  });
81
81
  };
82
82
 
83
+ // Width in pixels per toolbar item (icon width + spacing)
84
+ const TOOLBAR_ITEM_WIDTH = 19;
85
+
83
86
  const toolbarPositionToCSS = {
84
87
  inside: `
85
88
  top: 2px;
@@ -87,7 +90,7 @@ const toolbarPositionToCSS = {
87
90
  above: `
88
91
  top: 0px;
89
92
  transform: translateY(-100%);
90
- padding-bottom: 2px;
93
+ padding-bottom: 0px;
91
94
  margin-bottom: -2px;
92
95
  `,
93
96
  below: `
@@ -99,7 +102,7 @@ const toolbarPositionToCSS = {
99
102
  };
100
103
 
101
104
  // 使用与 NocoBase 一致的悬浮工具栏样式
102
- const floatContainerStyles = ({ showBackground, showBorder, ctx, toolbarPosition = 'inside' }) => css`
105
+ const floatContainerStyles = ({ showBackground, showBorder, ctx, toolbarPosition = 'inside', toolbarCount }) => css`
103
106
  position: relative;
104
107
  display: inline;
105
108
 
@@ -135,6 +138,7 @@ const floatContainerStyles = ({ showBackground, showBorder, ctx, toolbarPosition
135
138
  border: ${showBorder ? '2px solid var(--colorBorderSettingsHover)' : ''};
136
139
  border-radius: ${ctx.themeToken.borderRadiusLG}px;
137
140
  pointer-events: none;
141
+ min-width: ${TOOLBAR_ITEM_WIDTH * toolbarCount}px;
138
142
 
139
143
  &.nb-in-template {
140
144
  background: var(--colorTemplateBgSettingsHover);
@@ -580,9 +584,13 @@ const FlowsFloatContextMenuWithModel: React.FC<ModelProvidedProps> = observer(
580
584
  return (
581
585
  <div
582
586
  ref={containerRef}
583
- className={`${floatContainerStyles({ showBackground, showBorder, ctx: model.context, toolbarPosition })} ${
584
- hideMenu ? 'hide-parent-menu' : ''
585
- } ${hasButton ? 'has-button-child' : ''} ${className || ''}`}
587
+ className={`${floatContainerStyles({
588
+ showBackground,
589
+ showBorder,
590
+ ctx: model.context,
591
+ toolbarPosition,
592
+ toolbarCount: getToolbarCount(flowEngine, extraToolbarItems),
593
+ })} ${hideMenu ? 'hide-parent-menu' : ''} ${hasButton ? 'has-button-child' : ''} ${className || ''}`}
586
594
  style={containerStyle}
587
595
  data-has-float-menu="true"
588
596
  onMouseMove={handleChildHover}
@@ -668,3 +676,9 @@ const FlowsFloatContextMenuWithModelById: React.FC<ModelByIdProps> = observer(
668
676
  );
669
677
 
670
678
  export { FlowsFloatContextMenu };
679
+
680
+ function getToolbarCount(flowEngine, extraToolbarItems) {
681
+ const toolbarItems = flowEngine?.flowSettings?.getToolbarItems?.() || [];
682
+ const allToolbarItems = [...toolbarItems, ...(extraToolbarItems || [])];
683
+ return allToolbarItems.length;
684
+ }
@@ -159,13 +159,23 @@ export class CollectionFieldModel<T extends DefaultStructure = DefaultStructure>
159
159
  static getDefaultBindingByField(
160
160
  ctx: FlowEngineContext,
161
161
  collectionField: CollectionField,
162
- options: { useStrict?: boolean; fallbackToTargetTitleField?: boolean } = {},
162
+ options: {
163
+ useStrict?: boolean;
164
+ fallbackToTargetTitleField?: boolean;
165
+ targetCollectionTitleField?: CollectionField;
166
+ } = {},
163
167
  ): BindingOptions | null {
164
168
  if (options.fallbackToTargetTitleField) {
165
169
  const binding = this.getDefaultBindingByField(ctx, collectionField, { useStrict: true });
166
170
  if (!binding) {
167
- if (collectionField.isAssociationField() && collectionField.targetCollectionTitleField) {
168
- return this.getDefaultBindingByField(ctx, collectionField.targetCollectionTitleField);
171
+ if (
172
+ (collectionField.isAssociationField() && options?.targetCollectionTitleField) ||
173
+ collectionField.targetCollectionTitleField
174
+ ) {
175
+ return this.getDefaultBindingByField(
176
+ ctx,
177
+ options?.targetCollectionTitleField || collectionField.targetCollectionTitleField,
178
+ );
169
179
  }
170
180
  }
171
181
  return binding;
@@ -131,21 +131,24 @@ export class MultiRecordResource<TDataItem = any> extends BaseRecordResource<TDa
131
131
 
132
132
  async update(filterByTk: string | number, data: Partial<TDataItem>, options?: AxiosRequestConfig): Promise<void> {
133
133
  const collection = this.context.collection;
134
- const filterTargetKey = collection.filterTargetKey;
135
- let result = data;
134
+ const filterTargetKey = collection?.filterTargetKey;
136
135
  const tkData = collection?.getFilterByTK(this.context.record);
137
- if (Array.isArray(filterTargetKey)) {
138
- result = {
139
- ...data,
140
- ...(tkData || {}),
141
- };
142
- } else {
143
- result = {
144
- ...data,
145
- [filterTargetKey]: tkData,
146
- };
136
+ let result = data;
137
+
138
+ // 安全处理 filterTargetKey & tkData
139
+ if (collection && filterTargetKey) {
140
+ if (Array.isArray(filterTargetKey)) {
141
+ result = {
142
+ ...data,
143
+ ...(tkData || {}),
144
+ };
145
+ } else {
146
+ result = {
147
+ ...data,
148
+ [filterTargetKey]: tkData,
149
+ };
150
+ }
147
151
  }
148
- console.log(result);
149
152
 
150
153
  const config = this.mergeRequestConfig(
151
154
  {
@@ -40,18 +40,20 @@ export class SingleRecordResource<TData = any> extends BaseRecordResource<TData>
40
40
  config.params.filterByTk = this.getFilterByTk();
41
41
  actionName = 'update';
42
42
  const collection = this.context.collection;
43
- const filterTargetKey = collection.filterTargetKey;
43
+ const filterTargetKey = collection?.filterTargetKey;
44
44
  const tkData = collection?.getFilterByTK(this.context.record);
45
- if (Array.isArray(filterTargetKey)) {
46
- result = {
47
- ...data,
48
- ...(tkData || {}),
49
- };
50
- } else {
51
- result = {
52
- ...data,
53
- [filterTargetKey]: tkData,
54
- };
45
+ if (collection && filterTargetKey) {
46
+ if (Array.isArray(filterTargetKey)) {
47
+ result = {
48
+ ...data,
49
+ ...(tkData || {}),
50
+ };
51
+ } else {
52
+ result = {
53
+ ...data,
54
+ [filterTargetKey]: tkData,
55
+ };
56
+ }
55
57
  }
56
58
  }
57
59
  await this.runAction(actionName, {