@nocobase/client-v2 2.1.0-beta.29 → 2.1.0-beta.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.
Files changed (100) 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/actions/index.d.ts +1 -1
  10. package/es/flow/actions/linkageRules.d.ts +2 -0
  11. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +4 -0
  12. package/es/flow/admin-shell/admin-layout/AdminLayoutModel.d.ts +7 -0
  13. package/es/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.d.ts +5 -0
  14. package/es/flow/components/FieldAssignRulesEditor.d.ts +1 -0
  15. package/es/flow/internal/utils/enumOptionsUtils.d.ts +5 -0
  16. package/es/flow/models/actions/AssociationActionUtils.d.ts +5 -0
  17. package/es/flow/models/blocks/filter-form/FilterFormBlockModel.d.ts +4 -0
  18. package/es/flow/models/blocks/filter-manager/FilterManager.d.ts +5 -1
  19. package/es/flow/models/blocks/table/TableSelectModel.d.ts +8 -0
  20. package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +3 -0
  21. package/es/flow/models/fields/DisplayTitleFieldModel.d.ts +1 -1
  22. package/es/flow/models/utils/displayValueUtils.d.ts +12 -0
  23. package/es/flow-compat/passwordUtils.d.ts +1 -1
  24. package/es/index.mjs +122 -106
  25. package/es/utils/remotePlugins.d.ts +0 -4
  26. package/lib/index.js +121 -105
  27. package/package.json +6 -5
  28. package/src/BaseApplication.tsx +14 -8
  29. package/src/PluginManager.ts +1 -0
  30. package/src/__tests__/app.test.tsx +28 -1
  31. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +67 -46
  32. package/src/__tests__/remotePlugins.test.ts +29 -18
  33. package/src/__tests__/settings-center.test.tsx +30 -0
  34. package/src/components/form/DrawerFormLayout.tsx +103 -0
  35. package/src/components/form/EnvVariableInput.tsx +126 -0
  36. package/src/components/form/FileSizeInput.tsx +105 -0
  37. package/src/components/form/createFormRegistry.ts +60 -0
  38. package/src/components/form/index.tsx +14 -0
  39. package/src/components/index.ts +1 -1
  40. package/src/flow/__tests__/FlowRoute.test.tsx +4 -5
  41. package/src/flow/actions/__tests__/actionLinkageRules.race.repro.test.ts +199 -0
  42. package/src/flow/actions/__tests__/dataScopeFilter.test.ts +92 -13
  43. package/src/flow/actions/__tests__/linkageRules.formValueDrivenRefresh.test.ts +6 -1
  44. package/src/flow/actions/__tests__/linkageRules.menu.test.ts +90 -0
  45. package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +476 -1
  46. package/src/flow/actions/index.ts +2 -0
  47. package/src/flow/actions/linkageRules.tsx +316 -280
  48. package/src/flow/actions/linkageRulesFormValueRefresh.ts +2 -8
  49. package/src/flow/actions/setTargetDataScope.tsx +32 -3
  50. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +8 -1
  51. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +70 -12
  52. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +26 -87
  53. package/src/flow/admin-shell/admin-layout/AdminLayoutModel.tsx +11 -0
  54. package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +5 -1
  55. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +292 -31
  56. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +50 -12
  57. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +77 -56
  58. package/src/flow/components/AdminLayout.tsx +2 -2
  59. package/src/flow/components/FieldAssignRulesEditor.tsx +2 -0
  60. package/src/flow/components/FlowRoute.tsx +17 -4
  61. package/src/flow/components/__tests__/FieldAssignRulesEditor.test.tsx +81 -4
  62. package/src/flow/components/filter/LinkageFilterItem.tsx +9 -2
  63. package/src/flow/components/filter/VariableFilterItem.tsx +2 -6
  64. package/src/flow/components/filter/__tests__/LinkageFilterItem.test.tsx +71 -0
  65. package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +48 -0
  66. package/src/flow/internal/utils/__tests__/enumOptionsUtils.test.ts +10 -1
  67. package/src/flow/internal/utils/enumOptionsUtils.ts +29 -0
  68. package/src/flow/models/actions/AssociateActionModel.tsx +2 -2
  69. package/src/flow/models/actions/AssociationActionUtils.ts +14 -0
  70. package/src/flow/models/actions/__tests__/AssociationActionModel.test.ts +63 -0
  71. package/src/flow/models/base/CollectionBlockModel.tsx +7 -0
  72. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +33 -9
  73. package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +53 -13
  74. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.getFilterValue.test.ts +63 -3
  75. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +33 -1
  76. package/src/flow/models/blocks/filter-manager/FilterManager.ts +66 -2
  77. package/src/flow/models/blocks/filter-manager/__tests__/FilterManager.test.ts +270 -0
  78. package/src/flow/models/blocks/form/FormBlockModel.tsx +8 -5
  79. package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +30 -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/table/TableBlockModel.tsx +11 -6
  83. package/src/flow/models/blocks/table/TableColumnModel.tsx +3 -0
  84. package/src/flow/models/blocks/table/TableSelectModel.tsx +36 -26
  85. package/src/flow/models/blocks/table/__tests__/TableBlockModel.rowClick.test.ts +69 -0
  86. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +96 -1
  87. package/src/flow/models/blocks/table/__tests__/TableSelectModel.test.ts +41 -0
  88. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +4 -0
  89. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +7 -0
  90. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +4 -1
  91. package/src/flow/models/fields/ClickableFieldModel.tsx +9 -4
  92. package/src/flow/models/fields/DisplayTitleFieldModel.tsx +12 -4
  93. package/src/flow/models/fields/SelectFieldModel.tsx +31 -1
  94. package/src/flow/models/fields/__tests__/ClickableFieldModel.test.ts +23 -0
  95. package/src/flow/models/fields/mobile-components/MobileSelect.tsx +2 -1
  96. package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +7 -0
  97. package/src/flow/models/utils/displayValueUtils.ts +57 -0
  98. package/src/flow/system-settings/useSystemSettings.tsx +36 -1
  99. package/src/utils/globalDeps.ts +2 -0
  100. 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
  });
@@ -42,9 +42,11 @@ export {
42
42
  detailsFieldLinkageRules,
43
43
  linkageSetDetailsFieldProps,
44
44
  actionLinkageRules,
45
+ menuLinkageRules,
45
46
  blockLinkageRules,
46
47
  linkageSetBlockProps,
47
48
  linkageSetActionProps,
49
+ linkageSetMenuItemProps,
48
50
  linkageSetFieldProps,
49
51
  subFormLinkageSetFieldProps,
50
52
  linkageAssignField,