@nocobase/client-v2 2.1.0-beta.41 → 2.1.0-beta.43

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.
@@ -86,6 +86,14 @@ function getAssignFormTempFieldRefreshKey(originField: AssignFormTempOriginField
86
86
  ].join('|');
87
87
  }
88
88
 
89
+ function normalizeAssignFormInputValue(value: unknown) {
90
+ if (!value || typeof value !== 'object' || !('target' in value)) {
91
+ return value;
92
+ }
93
+ const target = (value as { target?: { value?: unknown } }).target;
94
+ return target && 'value' in target ? target.value : value;
95
+ }
96
+
89
97
  /**
90
98
  * 使用 FormItemModel 的“表单项”包装,内部渲染 VariableInput,并将“常量”映射到临时字段模型。
91
99
  */
@@ -230,7 +238,6 @@ export class AssignFormItemModel extends FormItemModel {
230
238
  fm?.setProps?.({
231
239
  disabled: false,
232
240
  readPretty: false,
233
- pattern: 'editable',
234
241
  updateAssociation: false,
235
242
  multiple: multi,
236
243
  });
@@ -260,9 +267,12 @@ export class AssignFormItemModel extends FormItemModel {
260
267
  if (fm) {
261
268
  fm.setProps?.({
262
269
  value: inputProps?.value,
263
- onChange: (...args: any[]) => {
264
- const next = args && args.length ? args[0] : undefined;
265
- inputProps?.onChange?.(next);
270
+ onChange: (...args: unknown[]) => {
271
+ const nextArg = args && args.length ? args[0] : undefined;
272
+ const nextValue = normalizeAssignFormInputValue(nextArg);
273
+ fm.setProps?.({ value: nextValue });
274
+ this.assignValue = nextValue;
275
+ inputProps?.onChange?.(nextValue);
266
276
  },
267
277
  });
268
278
  }
@@ -0,0 +1,216 @@
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 React from 'react';
11
+ import { App, ConfigProvider } from 'antd';
12
+ import { describe, expect, it } from 'vitest';
13
+ import { render, screen, userEvent, waitFor } from '@nocobase/test/client';
14
+ import {
15
+ FlowEngine,
16
+ FlowEngineProvider,
17
+ FlowRuntimeContext,
18
+ FlowSettingsContextProvider,
19
+ type FlowModel,
20
+ type IFlowModelRepository,
21
+ } from '@nocobase/flow-engine';
22
+ import { AssignFormGridModel } from '../AssignFormGridModel';
23
+ import { AssignFormItemModel } from '../AssignFormItemModel';
24
+ import { AssignFormModel } from '../AssignFormModel';
25
+ import { createAssignFieldValuesStep } from '../assignFieldValuesFlow';
26
+ import { InputFieldModel } from '../../../fields/InputFieldModel';
27
+ import { VariableFieldFormModel } from '../../../fields/VariableFieldFormModel';
28
+
29
+ class MockFlowModelRepository implements IFlowModelRepository {
30
+ async findOne(): Promise<Record<string, any> | null> {
31
+ return null;
32
+ }
33
+
34
+ async save(model: FlowModel): Promise<Record<string, any>> {
35
+ return { uid: model.uid };
36
+ }
37
+
38
+ async destroy(): Promise<boolean> {
39
+ return true;
40
+ }
41
+
42
+ async move(): Promise<void> {}
43
+
44
+ async duplicate(): Promise<Record<string, any> | null> {
45
+ return null;
46
+ }
47
+ }
48
+
49
+ describe('assignFieldValuesFlow (editor)', () => {
50
+ it('repairs AssignFormModel resource init and clears cached collection', async () => {
51
+ const engine = new FlowEngine();
52
+ engine.setModelRepository(new MockFlowModelRepository());
53
+ engine.registerModels({
54
+ AssignFormModel,
55
+ AssignFormGridModel,
56
+ AssignFormItemModel,
57
+ InputFieldModel,
58
+ VariableFieldFormModel,
59
+ });
60
+ engine.context.defineProperty('location', { value: { search: '' } });
61
+ engine.context.defineProperty('themeToken', { value: { marginLG: 24 } });
62
+
63
+ const dsm = engine.context.dataSourceManager;
64
+ const main = dsm.getDataSource('main');
65
+ main.addCollection({
66
+ name: 'users',
67
+ fields: [{ name: 'nickname', type: 'string', interface: 'input' }],
68
+ });
69
+ const users = dsm.getCollection('main', 'users');
70
+ expect(users?.name).toBe('users');
71
+
72
+ const action = engine.createModel({
73
+ use: 'FlowModel',
74
+ uid: 'act-assign',
75
+ });
76
+ action.setStepParams('assignSettings', 'assignFieldValues', { assignedValues: { nickname: '1111' } });
77
+
78
+ const existing = engine.createModel<AssignFormModel>({
79
+ use: 'AssignFormModel',
80
+ uid: 'form-assign',
81
+ parentId: action.uid,
82
+ subKey: 'assignForm',
83
+ stepParams: {
84
+ resourceSettings: {
85
+ init: {
86
+ dataSourceKey: 'main',
87
+ collectionName: 'missing',
88
+ },
89
+ },
90
+ },
91
+ });
92
+ expect(existing.context.collection).toBeUndefined();
93
+
94
+ action.context.defineProperty('blockModel', { value: { collection: users } });
95
+
96
+ const step = createAssignFieldValuesStep({ settingsFlowKey: 'assignSettings' });
97
+ const schema = step.uiSchema();
98
+ const Editor = schema.editor?.['x-component'] as React.ComponentType;
99
+ expect(Editor).toBeTypeOf('function');
100
+
101
+ const flowSettingsCtx = new FlowRuntimeContext(action as any, 'assignSettings', 'settings');
102
+
103
+ render(
104
+ <FlowEngineProvider engine={engine}>
105
+ <ConfigProvider>
106
+ <App>
107
+ <FlowSettingsContextProvider value={flowSettingsCtx}>
108
+ <Editor />
109
+ </FlowSettingsContextProvider>
110
+ </App>
111
+ </ConfigProvider>
112
+ </FlowEngineProvider>,
113
+ );
114
+
115
+ await waitFor(() => {
116
+ const form = engine.findModelByParentId<AssignFormModel>(action.uid, 'assignForm');
117
+ expect(form?.getStepParams('resourceSettings', 'init')).toEqual({
118
+ dataSourceKey: 'main',
119
+ collectionName: 'users',
120
+ });
121
+ expect(form?.context.collection?.name).toBe('users');
122
+ expect(form?.context.collection?.getFields?.().length).toBeGreaterThan(0);
123
+ });
124
+
125
+ await waitFor(() => {
126
+ expect(screen.getByText('nickname')).toBeInTheDocument();
127
+ const form = engine.findModelByParentId<AssignFormModel>(action.uid, 'assignForm');
128
+ expect(form?.subModels.grid.subModels.items.length).toBe(1);
129
+ });
130
+
131
+ await waitFor(() => {
132
+ expect(screen.getByDisplayValue('1111')).toBeInTheDocument();
133
+ });
134
+
135
+ const input = screen.getByDisplayValue('1111') as HTMLInputElement;
136
+ await userEvent.clear(input);
137
+ await userEvent.type(input, '2222');
138
+
139
+ await waitFor(() => {
140
+ const form = engine.findModelByParentId<AssignFormModel>(action.uid, 'assignForm');
141
+ expect(form?.getAssignedValues()).toEqual({ nickname: '2222' });
142
+ });
143
+ });
144
+
145
+ it('does not clear delegated parent resource cache when repairing AssignFormModel init', async () => {
146
+ const engine = new FlowEngine();
147
+ engine.setModelRepository(new MockFlowModelRepository());
148
+ engine.registerModels({ AssignFormModel, AssignFormGridModel, AssignFormItemModel, InputFieldModel });
149
+ engine.context.defineProperty('location', { value: { search: '' } });
150
+ engine.context.defineProperty('themeToken', { value: { marginLG: 24 } });
151
+
152
+ const dsm = engine.context.dataSourceManager;
153
+ const main = dsm.getDataSource('main');
154
+ main.addCollection({
155
+ name: 'users',
156
+ fields: [{ name: 'nickname', type: 'string', interface: 'input' }],
157
+ });
158
+ const users = dsm.getCollection('main', 'users');
159
+
160
+ const action = engine.createModel({
161
+ use: 'FlowModel',
162
+ uid: 'act-assign-parent-resource',
163
+ });
164
+
165
+ let resourceCreateCount = 0;
166
+ action.context.defineProperty('resource', {
167
+ get: () => ({ id: ++resourceCreateCount }),
168
+ });
169
+ const cachedResource = action.context.resource;
170
+
171
+ engine.createModel<AssignFormModel>({
172
+ use: 'AssignFormModel',
173
+ uid: 'form-assign-parent-resource',
174
+ parentId: action.uid,
175
+ subKey: 'assignForm',
176
+ stepParams: {
177
+ resourceSettings: {
178
+ init: {
179
+ dataSourceKey: 'main',
180
+ collectionName: 'missing',
181
+ },
182
+ },
183
+ },
184
+ });
185
+
186
+ action.context.defineProperty('blockModel', { value: { collection: users } });
187
+
188
+ const step = createAssignFieldValuesStep({ settingsFlowKey: 'assignSettings' });
189
+ const schema = step.uiSchema();
190
+ const Editor = schema.editor?.['x-component'] as React.ComponentType;
191
+ const flowSettingsCtx = new FlowRuntimeContext(action as any, 'assignSettings', 'settings');
192
+
193
+ render(
194
+ <FlowEngineProvider engine={engine}>
195
+ <ConfigProvider>
196
+ <App>
197
+ <FlowSettingsContextProvider value={flowSettingsCtx}>
198
+ <Editor />
199
+ </FlowSettingsContextProvider>
200
+ </App>
201
+ </ConfigProvider>
202
+ </FlowEngineProvider>,
203
+ );
204
+
205
+ await waitFor(() => {
206
+ const form = engine.findModelByParentId<AssignFormModel>(action.uid, 'assignForm');
207
+ expect(form?.getStepParams('resourceSettings', 'init')).toEqual({
208
+ dataSourceKey: 'main',
209
+ collectionName: 'users',
210
+ });
211
+ });
212
+
213
+ expect(action.context.resource).toBe(cachedResource);
214
+ expect(resourceCreateCount).toBe(1);
215
+ });
216
+ });
@@ -15,7 +15,7 @@ import {
15
15
  useFlowEngine,
16
16
  useFlowSettingsContext,
17
17
  } from '@nocobase/flow-engine';
18
- import React, { useEffect, useRef } from 'react';
18
+ import React, { useEffect } from 'react';
19
19
  import { CollectionActionModel } from '../../base/CollectionActionModel';
20
20
  import { RecordActionModel } from '../../base/RecordActionModel';
21
21
  import { AssignFormModel } from './AssignFormModel';
@@ -38,6 +38,9 @@ type AssignFieldValuesModel = {
38
38
  uid: string;
39
39
  assignFormUid?: string;
40
40
  context?: AssignFieldValuesContext;
41
+ subModels?: {
42
+ assignForm?: AssignFormModel;
43
+ };
41
44
  getStepParams?: (flowKey: string, stepKey: string) => { assignedValues?: AssignedValues } | undefined;
42
45
  setStepParams?: (flowKey: string, stepKey: string, params: { assignedValues: AssignedValues }) => void;
43
46
  };
@@ -62,6 +65,81 @@ function getResourceInit(ctx: AssignFieldValuesContext): { dataSourceKey: string
62
65
  return dsKey && collName ? { dataSourceKey: dsKey, collectionName: collName } : undefined;
63
66
  }
64
67
 
68
+ function resolveInitFromFlowSettings(
69
+ blockModel: unknown,
70
+ actionContext: AssignFieldValuesContext | undefined,
71
+ ): { dataSourceKey: string; collectionName: string } | undefined {
72
+ const maybeBlockCollection = (blockModel as { collection?: { dataSourceKey?: string; name?: string } } | undefined)
73
+ ?.collection;
74
+ const collection = maybeBlockCollection || getContextCollection(actionContext);
75
+ const dsKey = collection?.dataSourceKey;
76
+ const collName = collection?.name;
77
+ return dsKey && collName ? { dataSourceKey: dsKey, collectionName: collName } : undefined;
78
+ }
79
+
80
+ function isValidResourceInit(init: unknown): init is {
81
+ dataSourceKey: string;
82
+ collectionName: string;
83
+ } {
84
+ if (!init || typeof init !== 'object') return false;
85
+ const obj = init as { dataSourceKey?: unknown; collectionName?: unknown };
86
+ return (
87
+ typeof obj.dataSourceKey === 'string' &&
88
+ !!obj.dataSourceKey &&
89
+ typeof obj.collectionName === 'string' &&
90
+ !!obj.collectionName
91
+ );
92
+ }
93
+
94
+ type LocalCacheContext = {
95
+ _observableCache?: Record<string, unknown>;
96
+ _cache?: Record<string, unknown>;
97
+ _pending?: Record<string, unknown>;
98
+ };
99
+
100
+ function toLocalCacheContext(context: unknown): LocalCacheContext | undefined {
101
+ return context && typeof context === 'object' ? (context as LocalCacheContext) : undefined;
102
+ }
103
+
104
+ function clearOwnContextCache(context: unknown, key: string) {
105
+ const localContext = toLocalCacheContext(context);
106
+ if (!localContext) {
107
+ return;
108
+ }
109
+ delete localContext._observableCache?.[key];
110
+ delete localContext._cache?.[key];
111
+ delete localContext._pending?.[key];
112
+ }
113
+
114
+ function clearResourceContextCache(model: { context?: unknown } | undefined) {
115
+ clearOwnContextCache(model?.context, 'dataSource');
116
+ clearOwnContextCache(model?.context, 'collection');
117
+ clearOwnContextCache(model?.context, 'resource');
118
+ clearOwnContextCache(model?.context, 'association');
119
+ }
120
+
121
+ function resolveAssignFormModel(ctx: {
122
+ model: AssignFieldValuesModel;
123
+ engine: {
124
+ getModel?: (uid: string, fromRoot?: boolean) => AssignFormModel | undefined;
125
+ findModelByParentId?: (parentId: string, subKey: string) => AssignFormModel | undefined | null;
126
+ };
127
+ }) {
128
+ if (ctx.model.assignFormUid) {
129
+ const form = ctx.engine.getModel?.(ctx.model.assignFormUid, true);
130
+ if (form) {
131
+ return form;
132
+ }
133
+ }
134
+
135
+ const localForm = ctx.model.subModels?.assignForm;
136
+ if (localForm) {
137
+ return localForm;
138
+ }
139
+
140
+ return ctx.engine.findModelByParentId?.(ctx.model.uid, 'assignForm') || undefined;
141
+ }
142
+
65
143
  export function createAssignFormSubModelOptions(ctx: AssignFieldValuesContext) {
66
144
  return {
67
145
  async: true,
@@ -114,21 +192,52 @@ function AssignFieldsEditor(props: { settingsFlowKey: string; clearRecordContext
114
192
  const { model, blockModel } = useFlowSettingsContext();
115
193
  const action = model as AssignFieldValuesModel;
116
194
  const engine = useFlowEngine();
117
- const initializedRef = useRef(false);
118
195
  const [formModel, setFormModel] = React.useState<AssignFormModel | null>(null);
119
196
 
120
197
  useEffect(() => {
121
198
  let cancelled = false;
122
199
  const loadAssignForm = async () => {
200
+ const init = resolveInitFromFlowSettings(blockModel, action?.context);
123
201
  const loaded = (await engine.loadOrCreateModel(
124
- {
125
- parentId: action.uid,
126
- subKey: 'assignForm',
127
- use: 'AssignFormModel',
128
- },
202
+ init
203
+ ? {
204
+ parentId: action.uid,
205
+ subKey: 'assignForm',
206
+ use: 'AssignFormModel',
207
+ stepParams: { resourceSettings: { init } },
208
+ }
209
+ : {
210
+ parentId: action.uid,
211
+ subKey: 'assignForm',
212
+ use: 'AssignFormModel',
213
+ },
129
214
  { skipSave: !model.context.flowSettingsEnabled },
130
215
  )) as AssignFormModel;
131
216
  if (cancelled) return;
217
+
218
+ if (isValidResourceInit(init)) {
219
+ const existingInit = loaded.getStepParams?.('resourceSettings', 'init');
220
+ // Ensure `resourceSettings.init` is available before first render to avoid cached undefined `collection`
221
+ if (
222
+ !isValidResourceInit(existingInit) ||
223
+ existingInit.dataSourceKey !== init.dataSourceKey ||
224
+ existingInit.collectionName !== init.collectionName
225
+ ) {
226
+ loaded.setStepParams?.('resourceSettings', 'init', init);
227
+ }
228
+ clearResourceContextCache(loaded);
229
+ clearResourceContextCache(loaded.subModels?.grid);
230
+ }
231
+
232
+ const prev = action.getStepParams?.(props.settingsFlowKey, ASSIGN_FIELD_VALUES_STEP_KEY) || {};
233
+ loaded.setInitialAssignedValues(prev?.assignedValues || {});
234
+
235
+ const isBulkCollectionAction = model instanceof CollectionActionModel && !(model instanceof RecordActionModel);
236
+ const isBulk = props.clearRecordContext || isBulkCollectionAction;
237
+ if (isBulk && loaded.context?.defineProperty) {
238
+ loaded.context.defineProperty('record', { get: () => undefined, cache: false });
239
+ }
240
+
132
241
  setFormModel(loaded);
133
242
  action.assignFormUid = loaded?.uid || action.assignFormUid;
134
243
  };
@@ -136,31 +245,15 @@ function AssignFieldsEditor(props: { settingsFlowKey: string; clearRecordContext
136
245
  return () => {
137
246
  cancelled = true;
138
247
  };
139
- }, [action, engine, model.context.flowSettingsEnabled]);
140
-
141
- useEffect(() => {
142
- if (initializedRef.current) return;
143
- if (!formModel) return;
144
-
145
- const prev = action.getStepParams?.(props.settingsFlowKey, ASSIGN_FIELD_VALUES_STEP_KEY) || {};
146
- const coll = blockModel?.collection || getContextCollection(action?.context);
147
- const dsKey = coll?.dataSourceKey;
148
- const collName = coll?.name;
149
- if (dsKey && collName) {
150
- formModel.setStepParams('resourceSettings', 'init', {
151
- dataSourceKey: dsKey,
152
- collectionName: collName,
153
- });
154
- }
155
- formModel.setInitialAssignedValues(prev?.assignedValues || {});
156
-
157
- const isBulk =
158
- props.clearRecordContext || (action instanceof CollectionActionModel && !(action instanceof RecordActionModel));
159
- if (isBulk && formModel.context?.defineProperty) {
160
- formModel.context.defineProperty('record', { get: () => undefined, cache: false });
161
- }
162
- initializedRef.current = true;
163
- }, [action, blockModel?.collection, formModel, props.clearRecordContext, props.settingsFlowKey]);
248
+ }, [
249
+ action,
250
+ blockModel,
251
+ engine,
252
+ model,
253
+ model.context.flowSettingsEnabled,
254
+ props.clearRecordContext,
255
+ props.settingsFlowKey,
256
+ ]);
164
257
 
165
258
  return formModel ? <FlowModelRenderer model={formModel} showFlowSettings={false} /> : null;
166
259
  }
@@ -187,14 +280,23 @@ export function createAssignFieldValuesStep(options: AssignFieldValuesStepOption
187
280
  },
188
281
  };
189
282
  },
190
- async beforeParamsSave(ctx: {
191
- model: AssignFieldValuesModel;
192
- engine: {
193
- getModel?: (uid: string, fromRoot?: boolean) => AssignFormModel | undefined;
194
- };
195
- }) {
196
- const form = ctx.model.assignFormUid ? ctx.engine.getModel?.(ctx.model.assignFormUid, true) : undefined;
197
- if (!form) return;
283
+ async beforeParamsSave(
284
+ ctx: {
285
+ model: AssignFieldValuesModel;
286
+ engine: {
287
+ getModel?: (uid: string, fromRoot?: boolean) => AssignFormModel | undefined;
288
+ findModelByParentId?: (parentId: string, subKey: string) => AssignFormModel | undefined | null;
289
+ };
290
+ },
291
+ params?: { assignedValues?: AssignedValues },
292
+ previousParams?: { assignedValues?: AssignedValues },
293
+ ) {
294
+ const form = resolveAssignFormModel(ctx);
295
+ if (!form) {
296
+ const assignedValues = params?.assignedValues || previousParams?.assignedValues || {};
297
+ ctx.model.setStepParams?.(options.settingsFlowKey, ASSIGN_FIELD_VALUES_STEP_KEY, { assignedValues });
298
+ return;
299
+ }
198
300
  if (options.validateBeforeSave) {
199
301
  await form.form?.validateFields?.();
200
302
  }