@nocobase/client-v2 2.1.0-beta.30 → 2.1.0-beta.33

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.
Files changed (101) hide show
  1. package/es/BaseApplication.d.ts +1 -0
  2. package/es/PluginManager.d.ts +1 -0
  3. package/es/components/form/DrawerFormLayout.d.ts +49 -0
  4. package/es/components/form/EnvVariableInput.d.ts +42 -0
  5. package/es/components/form/FileSizeInput.d.ts +27 -0
  6. package/es/components/form/createFormRegistry.d.ts +33 -0
  7. package/es/components/form/index.d.ts +13 -0
  8. package/es/components/index.d.ts +1 -1
  9. package/es/flow/components/FieldAssignRulesEditor.d.ts +1 -0
  10. package/es/flow/components/code-editor/index.d.ts +1 -0
  11. package/es/flow/internal/utils/enumOptionsUtils.d.ts +5 -0
  12. package/es/flow/models/actions/AssociationActionUtils.d.ts +5 -0
  13. package/es/flow/models/blocks/filter-form/FilterFormBlockModel.d.ts +4 -0
  14. package/es/flow/models/blocks/filter-manager/FilterManager.d.ts +5 -1
  15. package/es/flow/models/blocks/shared/legacyDefaultValueMigrationBase.d.ts +1 -0
  16. package/es/flow/models/blocks/table/TableSelectModel.d.ts +8 -0
  17. package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +4 -0
  18. package/es/flow/models/fields/ClickableFieldModel.d.ts +3 -0
  19. package/es/flow/models/fields/DisplayEnumFieldModel.d.ts +6 -0
  20. package/es/flow/models/fields/DisplayTitleFieldModel.d.ts +2 -2
  21. package/es/flow/models/utils/displayValueUtils.d.ts +12 -0
  22. package/es/flow-compat/passwordUtils.d.ts +1 -1
  23. package/es/index.mjs +119 -108
  24. package/es/utils/remotePlugins.d.ts +0 -4
  25. package/lib/index.js +122 -111
  26. package/package.json +9 -5
  27. package/src/BaseApplication.tsx +14 -8
  28. package/src/PluginManager.ts +1 -0
  29. package/src/__tests__/app.test.tsx +28 -1
  30. package/src/__tests__/globalDeps.test.ts +1 -0
  31. package/src/__tests__/remotePlugins.test.ts +29 -18
  32. package/src/components/form/DrawerFormLayout.tsx +103 -0
  33. package/src/components/form/EnvVariableInput.tsx +126 -0
  34. package/src/components/form/FileSizeInput.tsx +105 -0
  35. package/src/components/form/createFormRegistry.ts +60 -0
  36. package/src/components/form/index.tsx +14 -0
  37. package/src/components/index.ts +1 -1
  38. package/src/flow/actions/__tests__/dataScopeFilter.test.ts +92 -13
  39. package/src/flow/actions/__tests__/formAssignRules.legacyMigration.test.tsx +173 -0
  40. package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +476 -1
  41. package/src/flow/actions/__tests__/pattern.test.ts +134 -0
  42. package/src/flow/actions/__tests__/titleField.test.ts +45 -0
  43. package/src/flow/actions/filterFormDefaultValues.tsx +30 -9
  44. package/src/flow/actions/formAssignRules.tsx +24 -9
  45. package/src/flow/actions/linkageRules.tsx +240 -258
  46. package/src/flow/actions/pattern.tsx +41 -6
  47. package/src/flow/actions/setTargetDataScope.tsx +32 -3
  48. package/src/flow/actions/titleField.tsx +4 -2
  49. package/src/flow/actions/validation.tsx +1 -1
  50. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +0 -4
  51. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +13 -5
  52. package/src/flow/components/DynamicFlowsIcon.tsx +87 -13
  53. package/src/flow/components/FieldAssignRulesEditor.tsx +2 -0
  54. package/src/flow/components/__tests__/DynamicFlowsIcon.test.tsx +195 -8
  55. package/src/flow/components/__tests__/FieldAssignRulesEditor.test.tsx +81 -4
  56. package/src/flow/components/code-editor/index.tsx +12 -8
  57. package/src/flow/components/filter/LinkageFilterItem.tsx +9 -2
  58. package/src/flow/components/filter/VariableFilterItem.tsx +2 -6
  59. package/src/flow/components/filter/__tests__/LinkageFilterItem.test.tsx +71 -0
  60. package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +48 -0
  61. package/src/flow/internal/utils/__tests__/enumOptionsUtils.test.ts +10 -1
  62. package/src/flow/internal/utils/enumOptionsUtils.ts +29 -0
  63. package/src/flow/models/actions/AssociateActionModel.tsx +2 -2
  64. package/src/flow/models/actions/AssociationActionUtils.ts +14 -0
  65. package/src/flow/models/actions/__tests__/AssociationActionModel.test.ts +63 -0
  66. package/src/flow/models/base/CollectionBlockModel.tsx +7 -0
  67. package/src/flow/models/base/PageModel/RootPageModel.tsx +1 -1
  68. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +33 -9
  69. package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +53 -13
  70. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.getFilterValue.test.ts +63 -3
  71. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +33 -1
  72. package/src/flow/models/blocks/filter-form/__tests__/legacyDefaultValueMigration.test.ts +3 -0
  73. package/src/flow/models/blocks/filter-manager/FilterManager.ts +66 -2
  74. package/src/flow/models/blocks/filter-manager/__tests__/FilterManager.test.ts +270 -0
  75. package/src/flow/models/blocks/form/FormActionModel.tsx +2 -8
  76. package/src/flow/models/blocks/form/FormBlockModel.tsx +8 -5
  77. package/src/flow/models/blocks/form/FormItemModel.tsx +1 -1
  78. package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +30 -0
  79. package/src/flow/models/blocks/form/__tests__/legacyDefaultValueMigration.test.ts +3 -0
  80. package/src/flow/models/blocks/form/value-runtime/rules.ts +6 -1
  81. package/src/flow/models/blocks/form/value-runtime/runtime.ts +6 -1
  82. package/src/flow/models/blocks/shared/legacyDefaultValueMigrationBase.ts +21 -5
  83. package/src/flow/models/blocks/table/TableBlockModel.tsx +11 -6
  84. package/src/flow/models/blocks/table/TableColumnModel.tsx +8 -2
  85. package/src/flow/models/blocks/table/TableSelectModel.tsx +36 -26
  86. package/src/flow/models/blocks/table/__tests__/TableBlockModel.rowClick.test.ts +69 -0
  87. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +132 -1
  88. package/src/flow/models/blocks/table/__tests__/TableSelectModel.test.ts +41 -0
  89. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +144 -3
  90. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableColumnModel.rowRecord.test.ts +170 -1
  91. package/src/flow/models/fields/ClickableFieldModel.tsx +55 -6
  92. package/src/flow/models/fields/DisplayEnumFieldModel.tsx +44 -0
  93. package/src/flow/models/fields/DisplayTitleFieldModel.tsx +40 -7
  94. package/src/flow/models/fields/SelectFieldModel.tsx +31 -1
  95. package/src/flow/models/fields/__tests__/ClickableFieldModel.test.ts +202 -1
  96. package/src/flow/models/fields/__tests__/DisplayEnumFieldModel.test.tsx +39 -0
  97. package/src/flow/models/fields/mobile-components/MobileSelect.tsx +2 -1
  98. package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +7 -0
  99. package/src/flow/models/utils/displayValueUtils.ts +57 -0
  100. package/src/utils/globalDeps.ts +11 -0
  101. package/src/utils/remotePlugins.ts +7 -27
@@ -7,8 +7,11 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
+ import React from 'react';
11
+ import { fireEvent, render, screen } from '@testing-library/react';
12
+ import { FlowSettingsContextProvider } from '@nocobase/flow-engine';
10
13
  import { describe, expect, it, vi } from 'vitest';
11
- import { subFormLinkageSetFieldProps } from '../linkageRules';
14
+ import { fieldLinkageRules, linkageSetFieldProps, subFormLinkageSetFieldProps } from '../linkageRules';
12
15
 
13
16
  describe('subFormLinkageSetFieldProps action', () => {
14
17
  it('should not throw when engine.getModel returns undefined', () => {
@@ -106,4 +109,476 @@ describe('subFormLinkageSetFieldProps action', () => {
106
109
  expect(getFork).toHaveBeenCalledWith('roles:0:field-2');
107
110
  expect(setProps).toHaveBeenCalledWith(formItemModel, { disabled: false });
108
111
  });
112
+
113
+ it('should limit options on the fork model', () => {
114
+ const setProps = vi.fn();
115
+ const selectedOptions = [{ label: 'Open', value: 'open' }];
116
+ const forkModel: any = {
117
+ uid: 'field-3',
118
+ isFork: true,
119
+ };
120
+ const formItemModel: any = {
121
+ uid: 'field-3',
122
+ getFork: vi.fn(() => forkModel),
123
+ };
124
+
125
+ const ctx: any = {
126
+ model: {
127
+ context: {
128
+ fieldKey: ['roles:0'],
129
+ },
130
+ },
131
+ engine: {
132
+ getModel: vi.fn(() => formItemModel),
133
+ },
134
+ };
135
+
136
+ subFormLinkageSetFieldProps.handler(ctx, {
137
+ value: {
138
+ fields: ['field-3'],
139
+ state: 'limitOptions',
140
+ selectedOptions,
141
+ },
142
+ setProps,
143
+ });
144
+
145
+ expect(setProps).toHaveBeenCalledWith(forkModel, { options: selectedOptions });
146
+ });
147
+ });
148
+
149
+ describe('linkageSetFieldProps action', () => {
150
+ it('should limit field options', () => {
151
+ const setProps = vi.fn();
152
+ const selectedOptions = [{ label: 'Draft', value: 'draft' }];
153
+ const fieldComponentModel: any = {
154
+ uid: 'status-field-component',
155
+ };
156
+ const fieldModel: any = {
157
+ uid: 'status-field',
158
+ subModels: {
159
+ field: fieldComponentModel,
160
+ },
161
+ };
162
+ const ctx: any = {
163
+ model: {
164
+ subModels: {
165
+ grid: {
166
+ subModels: {
167
+ items: [fieldModel],
168
+ },
169
+ },
170
+ },
171
+ },
172
+ };
173
+
174
+ linkageSetFieldProps.handler(ctx, {
175
+ value: {
176
+ fields: ['status-field'],
177
+ state: 'limitOptions',
178
+ selectedOptions,
179
+ },
180
+ setProps,
181
+ });
182
+
183
+ expect(setProps).toHaveBeenCalledWith(fieldComponentModel, { options: selectedOptions });
184
+ });
185
+
186
+ it('should sync limited options to existing field forks immediately', async () => {
187
+ const selectedOptions = [{ label: 'Draft', value: 'draft' }];
188
+ const forkModel: any = {
189
+ uid: 'status-field-component',
190
+ isFork: true,
191
+ setProps: vi.fn(),
192
+ };
193
+ const fieldComponentModel: any = {
194
+ uid: 'status-field-component',
195
+ props: {
196
+ options: [
197
+ { label: 'Draft', value: 'draft' },
198
+ { label: 'Published', value: 'published' },
199
+ ],
200
+ },
201
+ forks: new Set([forkModel]),
202
+ setProps(key: any, value?: any) {
203
+ if (typeof key === 'string') {
204
+ this.props[key] = value;
205
+ } else {
206
+ this.props = { ...this.props, ...key };
207
+ }
208
+ },
209
+ };
210
+ const staleFieldComponentModel: any = {
211
+ uid: 'status-field-component',
212
+ props: {
213
+ options: [
214
+ { label: 'Draft', value: 'draft' },
215
+ { label: 'Published', value: 'published' },
216
+ ],
217
+ },
218
+ forks: new Set(),
219
+ setProps: vi.fn(),
220
+ };
221
+ const fieldModel: any = {
222
+ uid: 'status-field',
223
+ subModels: {
224
+ field: fieldComponentModel,
225
+ },
226
+ };
227
+ const ctx: any = {
228
+ app: {
229
+ jsonLogic: {
230
+ apply: vi.fn(() => true),
231
+ },
232
+ },
233
+ model: {
234
+ __allModels: [staleFieldComponentModel],
235
+ subModels: {
236
+ grid: {
237
+ subModels: {
238
+ items: [fieldModel],
239
+ },
240
+ },
241
+ },
242
+ },
243
+ getAction: (name: string) => (name === 'linkageSetFieldProps' ? linkageSetFieldProps : null),
244
+ resolveJsonTemplate: vi.fn(async (value) => value),
245
+ };
246
+
247
+ await fieldLinkageRules.handler(ctx, {
248
+ value: [
249
+ {
250
+ key: 'rule-1',
251
+ enable: true,
252
+ condition: { logic: '$and', items: [] },
253
+ actions: [
254
+ {
255
+ key: 'action-1',
256
+ name: 'linkageSetFieldProps',
257
+ params: {
258
+ value: {
259
+ fields: ['status-field'],
260
+ state: 'limitOptions',
261
+ selectedOptions,
262
+ },
263
+ },
264
+ },
265
+ ],
266
+ },
267
+ ],
268
+ });
269
+
270
+ expect(fieldComponentModel.props.options).toEqual(selectedOptions);
271
+ expect(staleFieldComponentModel.setProps).not.toHaveBeenCalledWith({ options: selectedOptions });
272
+ expect(forkModel.setProps).toHaveBeenCalledWith({ options: selectedOptions });
273
+ });
274
+
275
+ it('should not clear form value when limit options reruns after selection', async () => {
276
+ const selectedOptions = [{ label: 'Draft', value: 'draft' }];
277
+ const form = {
278
+ getFieldValue: vi.fn(() => 'draft'),
279
+ setFieldValue: vi.fn(),
280
+ };
281
+ const fieldComponentModel: any = {
282
+ uid: 'status-field-component',
283
+ context: { form },
284
+ props: {
285
+ name: 'status',
286
+ value: undefined,
287
+ options: [
288
+ { label: 'Draft', value: 'draft' },
289
+ { label: 'Published', value: 'published' },
290
+ ],
291
+ },
292
+ setProps(key: any, value?: any) {
293
+ if (typeof key === 'string') {
294
+ this.props[key] = value;
295
+ } else {
296
+ this.props = { ...this.props, ...key };
297
+ }
298
+ },
299
+ };
300
+ const fieldModel: any = {
301
+ uid: 'status-field',
302
+ subModels: {
303
+ field: fieldComponentModel,
304
+ },
305
+ };
306
+ const ctx: any = {
307
+ app: {
308
+ jsonLogic: {
309
+ apply: vi.fn(() => true),
310
+ },
311
+ },
312
+ model: {
313
+ context: { form },
314
+ subModels: {
315
+ grid: {
316
+ subModels: {
317
+ items: [fieldModel],
318
+ },
319
+ },
320
+ },
321
+ },
322
+ getAction: (name: string) => (name === 'linkageSetFieldProps' ? linkageSetFieldProps : null),
323
+ resolveJsonTemplate: vi.fn(async (value) => value),
324
+ };
325
+
326
+ await fieldLinkageRules.handler(ctx, {
327
+ value: [
328
+ {
329
+ key: 'rule-1',
330
+ enable: true,
331
+ condition: { logic: '$and', items: [] },
332
+ actions: [
333
+ {
334
+ key: 'action-1',
335
+ name: 'linkageSetFieldProps',
336
+ params: {
337
+ value: {
338
+ fields: ['status-field'],
339
+ state: 'limitOptions',
340
+ selectedOptions,
341
+ },
342
+ },
343
+ },
344
+ ],
345
+ },
346
+ ],
347
+ });
348
+
349
+ expect(fieldComponentModel.props.options).toEqual(selectedOptions);
350
+ expect(form.setFieldValue).not.toHaveBeenCalled();
351
+ });
352
+
353
+ it('should keep all options selectable after options were limited once', async () => {
354
+ const fieldComponentModel: any = {
355
+ uid: 'status-field-component',
356
+ props: {
357
+ options: [{ label: 'Draft', value: 'draft' }],
358
+ },
359
+ };
360
+ const fieldModel: any = {
361
+ uid: 'status-field',
362
+ props: {
363
+ label: 'Status',
364
+ },
365
+ collectionField: {
366
+ interface: 'select',
367
+ uiSchema: {
368
+ enum: [
369
+ { label: 'Draft', value: 'draft' },
370
+ { label: 'Published', value: 'published' },
371
+ ],
372
+ },
373
+ },
374
+ subModels: {
375
+ field: fieldComponentModel,
376
+ },
377
+ };
378
+ const ctx: any = {
379
+ model: {
380
+ translate: (text: string) => text,
381
+ subModels: {
382
+ grid: {
383
+ subModels: {
384
+ items: [fieldModel],
385
+ },
386
+ },
387
+ },
388
+ },
389
+ };
390
+ const Comp: any = linkageSetFieldProps.uiSchema.value['x-component'];
391
+ const value = {
392
+ fields: ['status-field'],
393
+ state: 'limitOptions',
394
+ selectedOptions: [{ label: 'Draft', value: 'draft' }],
395
+ };
396
+ const view = render(
397
+ React.createElement(
398
+ FlowSettingsContextProvider,
399
+ { value: ctx },
400
+ React.createElement(Comp, { value, onChange: () => {} }),
401
+ ),
402
+ );
403
+
404
+ const selectors = view.container.querySelectorAll('.ant-select-selector');
405
+ fireEvent.mouseDown(selectors[2] as Element);
406
+ expect(await screen.findByText('Published')).toBeTruthy();
407
+ });
408
+
409
+ it('should only show options state for supported single field selection', () => {
410
+ const supportedField: any = {
411
+ uid: 'status-field',
412
+ props: {
413
+ label: 'Status',
414
+ },
415
+ collectionField: {
416
+ interface: 'select',
417
+ uiSchema: {
418
+ enum: [{ label: 'Draft', value: 'draft' }],
419
+ },
420
+ },
421
+ subModels: {
422
+ field: {
423
+ uid: 'status-field-component',
424
+ props: {
425
+ options: [{ label: 'Draft', value: 'draft' }],
426
+ },
427
+ },
428
+ },
429
+ };
430
+ const unsupportedField: any = {
431
+ uid: 'note-field',
432
+ props: {
433
+ label: 'Note',
434
+ },
435
+ collectionField: {
436
+ interface: 'input',
437
+ },
438
+ subModels: {
439
+ field: {
440
+ uid: 'note-field-component',
441
+ props: {},
442
+ },
443
+ },
444
+ };
445
+ const ctx: any = {
446
+ model: {
447
+ translate: (text: string) => text,
448
+ subModels: {
449
+ grid: {
450
+ subModels: {
451
+ items: [supportedField, unsupportedField],
452
+ },
453
+ },
454
+ },
455
+ },
456
+ };
457
+ const Comp: any = linkageSetFieldProps.uiSchema.value['x-component'];
458
+
459
+ const { rerender } = render(
460
+ React.createElement(
461
+ FlowSettingsContextProvider,
462
+ { value: ctx },
463
+ React.createElement(Comp, {
464
+ value: {
465
+ fields: ['status-field'],
466
+ state: 'limitOptions',
467
+ },
468
+ onChange: () => {},
469
+ }),
470
+ ),
471
+ );
472
+
473
+ expect(screen.getAllByText('Options')).toHaveLength(2);
474
+
475
+ rerender(
476
+ React.createElement(
477
+ FlowSettingsContextProvider,
478
+ { value: ctx },
479
+ React.createElement(Comp, {
480
+ value: {
481
+ fields: ['status-field', 'note-field'],
482
+ state: 'limitOptions',
483
+ },
484
+ onChange: () => {},
485
+ }),
486
+ ),
487
+ );
488
+
489
+ expect(screen.queryAllByText('Options')).toHaveLength(1);
490
+ });
491
+
492
+ it('should clear selected options when changing fields under limit options', () => {
493
+ const statusField: any = {
494
+ uid: 'status-field',
495
+ props: {
496
+ label: 'Status',
497
+ },
498
+ collectionField: {
499
+ interface: 'select',
500
+ uiSchema: {
501
+ enum: [
502
+ { label: 'Draft', value: 'draft' },
503
+ { label: 'Published', value: 'published' },
504
+ ],
505
+ },
506
+ },
507
+ subModels: {
508
+ field: {
509
+ uid: 'status-field-component',
510
+ props: {
511
+ options: [
512
+ { label: 'Draft', value: 'draft' },
513
+ { label: 'Published', value: 'published' },
514
+ ],
515
+ },
516
+ },
517
+ },
518
+ };
519
+ const categoryField: any = {
520
+ uid: 'category-field',
521
+ props: {
522
+ label: 'Category',
523
+ },
524
+ collectionField: {
525
+ interface: 'select',
526
+ uiSchema: {
527
+ enum: [
528
+ { label: 'A', value: 'a' },
529
+ { label: 'B', value: 'b' },
530
+ ],
531
+ },
532
+ },
533
+ subModels: {
534
+ field: {
535
+ uid: 'category-field-component',
536
+ props: {
537
+ options: [
538
+ { label: 'A', value: 'a' },
539
+ { label: 'B', value: 'b' },
540
+ ],
541
+ },
542
+ },
543
+ },
544
+ };
545
+ const ctx: any = {
546
+ model: {
547
+ translate: (text: string) => text,
548
+ subModels: {
549
+ grid: {
550
+ subModels: {
551
+ items: [statusField, categoryField],
552
+ },
553
+ },
554
+ },
555
+ },
556
+ };
557
+ const onChange = vi.fn();
558
+ const Comp: any = linkageSetFieldProps.uiSchema.value['x-component'];
559
+ const view = render(
560
+ React.createElement(
561
+ FlowSettingsContextProvider,
562
+ { value: ctx },
563
+ React.createElement(Comp, {
564
+ value: {
565
+ fields: ['status-field'],
566
+ state: 'limitOptions',
567
+ selectedOptions: [{ label: 'Draft', value: 'draft' }],
568
+ },
569
+ onChange,
570
+ }),
571
+ ),
572
+ );
573
+
574
+ const selectors = view.container.querySelectorAll('.ant-select-selector');
575
+ fireEvent.mouseDown(selectors[0] as Element);
576
+ fireEvent.click(screen.getByText('Category'));
577
+
578
+ expect(onChange).toHaveBeenLastCalledWith({
579
+ fields: ['status-field', 'category-field'],
580
+ state: undefined,
581
+ selectedOptions: [],
582
+ });
583
+ });
109
584
  });
@@ -187,4 +187,138 @@ describe('pattern action', () => {
187
187
 
188
188
  getDisplayBindingSpy.mockRestore();
189
189
  });
190
+
191
+ it('falls back to the target collection title field for association display only mode', async () => {
192
+ const engine = new FlowEngine();
193
+ engine.registerModels({
194
+ DummyFormItemModel,
195
+ FieldModel,
196
+ });
197
+
198
+ const parent = engine.createModel<DummyFormItemModel>({
199
+ use: DummyFormItemModel,
200
+ uid: 'form-item-association',
201
+ subModels: {
202
+ field: {
203
+ use: FieldModel,
204
+ uid: 'field-association',
205
+ props: {},
206
+ },
207
+ },
208
+ });
209
+ const collectionField = {
210
+ targetCollectionTitleFieldName: 'name',
211
+ isAssociationField: () => true,
212
+ };
213
+ parent.collectionField = collectionField;
214
+
215
+ await pattern.beforeParamsSave?.(
216
+ {
217
+ model: parent,
218
+ collectionField,
219
+ } as any,
220
+ { pattern: 'readPretty' },
221
+ { pattern: 'editable' },
222
+ );
223
+
224
+ expect(parent.props).toMatchObject({
225
+ pattern: 'readPretty',
226
+ disabled: false,
227
+ titleField: 'name',
228
+ });
229
+ expect(parent.getStepParams('editItemSettings', 'titleField')).toEqual({
230
+ titleField: 'name',
231
+ });
232
+ });
233
+
234
+ it('passes the association title field to the rebuilt display field model', async () => {
235
+ const engine = new FlowEngine();
236
+ engine.registerModels({
237
+ DummyFormItemModel,
238
+ FieldModel,
239
+ DummyDisplayFieldModel,
240
+ });
241
+
242
+ const parent = engine.createModel<DummyFormItemModel>({
243
+ use: DummyFormItemModel,
244
+ uid: 'form-item-association-rebuild',
245
+ subModels: {
246
+ field: {
247
+ use: FieldModel,
248
+ uid: 'field-association-rebuild',
249
+ props: {},
250
+ },
251
+ },
252
+ });
253
+ const targetTitleField = { name: 'name' };
254
+ const collectionField = {
255
+ targetCollectionTitleFieldName: 'name',
256
+ targetCollection: {
257
+ getField: vi.fn(() => targetTitleField),
258
+ },
259
+ isAssociationField: () => true,
260
+ };
261
+ parent.collectionField = collectionField;
262
+ const getDisplayBindingSpy = vi.spyOn(DetailsItemModel, 'getDefaultBindingByField').mockReturnValue({
263
+ modelName: 'DummyDisplayFieldModel',
264
+ defaultProps: { display: true },
265
+ } as any);
266
+
267
+ await pattern.afterParamsSave?.(
268
+ {
269
+ model: parent,
270
+ collectionField,
271
+ engine,
272
+ } as any,
273
+ { pattern: 'readPretty' },
274
+ { pattern: 'editable' },
275
+ );
276
+
277
+ expect(parent.subModels.field).toBeInstanceOf(DummyDisplayFieldModel);
278
+ expect(parent.subModels.field?.props).toMatchObject({
279
+ display: true,
280
+ titleField: 'name',
281
+ });
282
+
283
+ getDisplayBindingSpy.mockRestore();
284
+ });
285
+
286
+ it('refreshes the parent model after leaving display only mode', async () => {
287
+ const engine = new FlowEngine();
288
+ engine.registerModels({
289
+ DummyFormItemModel,
290
+ FieldModel,
291
+ DummyDisplayFieldModel,
292
+ });
293
+
294
+ const host = engine.createModel<FlowModel>({
295
+ use: FlowModel,
296
+ uid: 'sub-table-host',
297
+ });
298
+ const parent = engine.createModel<DummyFormItemModel>({
299
+ use: DummyFormItemModel,
300
+ uid: 'sub-table-column',
301
+ subModels: {
302
+ field: {
303
+ use: FieldModel,
304
+ uid: 'sub-table-column-field',
305
+ stepParams: {
306
+ fieldBinding: {
307
+ use: 'DummyDisplayFieldModel',
308
+ },
309
+ },
310
+ },
311
+ },
312
+ });
313
+ parent.setParent(host);
314
+ const hostSetPropsSpy = vi.spyOn(host, 'setProps');
315
+
316
+ await pattern.afterParamsSave?.(makeCtx(parent), { pattern: 'editable' }, { pattern: 'readPretty' });
317
+
318
+ expect(parent.subModels.field).toBeInstanceOf(FieldModel);
319
+ expect(parent.subModels.field?.uid).toBe('sub-table-column-field');
320
+ expect(hostSetPropsSpy).toHaveBeenCalledWith({
321
+ __patternRefreshKey: expect.any(String),
322
+ });
323
+ });
190
324
  });
@@ -0,0 +1,45 @@
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 { describe, expect, it, vi } from 'vitest';
11
+ import { titleField } from '../titleField';
12
+
13
+ describe('titleField action', () => {
14
+ it('builds options from target field interface metadata', () => {
15
+ const titleableField = {
16
+ name: 'nickname',
17
+ title: 'Nickname',
18
+ getInterfaceOptions: vi.fn(() => ({ titleUsable: true })),
19
+ };
20
+ const nonTitleableField = {
21
+ name: 'profile',
22
+ title: 'Profile',
23
+ getInterfaceOptions: vi.fn(() => ({ titleUsable: false })),
24
+ };
25
+ const missingContextManager = {
26
+ collectionFieldInterfaceManager: {
27
+ getFieldInterface: vi.fn(() => undefined),
28
+ },
29
+ };
30
+
31
+ const uiMode = (titleField as any).uiMode({
32
+ dataSourceManager: missingContextManager,
33
+ collectionField: {
34
+ targetCollection: {
35
+ getFields: () => [titleableField, nonTitleableField],
36
+ },
37
+ },
38
+ });
39
+
40
+ expect(uiMode.props.options).toEqual([{ value: 'nickname', label: 'Nickname' }]);
41
+ expect(titleableField.getInterfaceOptions).toHaveBeenCalled();
42
+ expect(nonTitleableField.getInterfaceOptions).toHaveBeenCalled();
43
+ expect(missingContextManager.collectionFieldInterfaceManager.getFieldInterface).not.toHaveBeenCalled();
44
+ });
45
+ });