@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
@@ -46,6 +46,7 @@ import { useAssociationTitleFieldSync } from '../components/useAssociationTitleF
46
46
  import _ from 'lodash';
47
47
  import { SubFormFieldModel, SubFormListFieldModel } from '../models';
48
48
  import { coerceForToOneField } from '../internal/utils/associationValueCoercion';
49
+ import { enumToOptions } from '../internal/utils/enumOptionsUtils';
49
50
  import {
50
51
  findFormItemModelByFieldPath,
51
52
  getCollectionFromModel,
@@ -120,6 +121,214 @@ const getFormFields = (ctx: any) => {
120
121
  }
121
122
  };
122
123
 
124
+ const cloneOptions = (options: any[]) =>
125
+ options.map((option) => (option && typeof option === 'object' ? { ...option } : option));
126
+
127
+ const LIMIT_OPTIONS_INTERFACES = new Set(['select', 'multipleSelect', 'radioGroup', 'checkboxGroup']);
128
+
129
+ const getFieldInterface = (fieldModel: any) => {
130
+ return fieldModel?.collectionField?.interface || fieldModel?.subModels?.field?.context?.collectionField?.interface;
131
+ };
132
+
133
+ const supportsLimitOptions = (fieldModel: any) => {
134
+ return LIMIT_OPTIONS_INTERFACES.has(getFieldInterface(fieldModel));
135
+ };
136
+
137
+ const getOriginalFieldOptions = (fieldModel: any) => {
138
+ const field = fieldModel?.subModels?.field || fieldModel;
139
+ const enumOptions = enumToOptions(fieldModel?.collectionField?.uiSchema?.enum, (text) => text) || [];
140
+ if (enumOptions.length > 0) {
141
+ field._originalOptionsFallback = cloneOptions(enumOptions);
142
+ return field._originalOptionsFallback;
143
+ }
144
+ if (Array.isArray(field?._originalOptionsFallback) && field._originalOptionsFallback.length > 0) {
145
+ return field._originalOptionsFallback;
146
+ }
147
+ if (Array.isArray(field?.props?.options) && field.props.options.length > 0) {
148
+ field._originalOptionsFallback = cloneOptions(field.props.options);
149
+ return field._originalOptionsFallback;
150
+ }
151
+ return null;
152
+ };
153
+
154
+ const getFieldModelOptions = (fieldModel: any, t: (s: string) => string) => {
155
+ const originalOptions = getOriginalFieldOptions(fieldModel);
156
+ if (originalOptions) {
157
+ return originalOptions.map((option: any) =>
158
+ option && typeof option === 'object' && typeof option.label === 'string'
159
+ ? { ...option, label: t(option.label) }
160
+ : option,
161
+ );
162
+ }
163
+ return [];
164
+ };
165
+
166
+ const getFieldOptionsBySelectedFields = (fieldOptions: any[], fields: string[] = [], t: (s: string) => string) => {
167
+ const selected = fields
168
+ .map((fieldUid) => fieldOptions.find((option) => option.value === fieldUid)?.model)
169
+ .filter((model) => supportsLimitOptions(model))
170
+ .filter(Boolean);
171
+
172
+ const options = selected.flatMap((model) => getFieldModelOptions(model, t));
173
+ const deduped = new Map<string, any>();
174
+ options.forEach((option) => {
175
+ deduped.set(JSON.stringify(option?.value), option);
176
+ });
177
+ return Array.from(deduped.values());
178
+ };
179
+
180
+ const getFieldStateProps = (state: string, selectedOptions?: any[]) => {
181
+ switch (state) {
182
+ case 'visible':
183
+ return { hiddenModel: false };
184
+ case 'hidden':
185
+ return { hiddenModel: true };
186
+ case 'hiddenReservedValue':
187
+ return { hidden: true };
188
+ case 'required':
189
+ return { required: true };
190
+ case 'notRequired':
191
+ return { required: false };
192
+ case 'disabled':
193
+ return { disabled: true };
194
+ case 'enabled':
195
+ return { disabled: false };
196
+ case 'limitOptions':
197
+ return Array.isArray(selectedOptions) ? { options: selectedOptions } : {};
198
+ default:
199
+ return null;
200
+ }
201
+ };
202
+
203
+ const getFieldStateTargetModel = (state: string, model: any) => {
204
+ return state === 'limitOptions' ? model?.subModels?.field || model : model;
205
+ };
206
+
207
+ const isFieldOptionsPatch = (props: any) => {
208
+ return props && typeof props === 'object' && Object.keys(props).length === 1 && Array.isArray(props.options);
209
+ };
210
+
211
+ const syncFieldOptionsToForks = (model: any, props: any) => {
212
+ if (!isFieldOptionsPatch(props) || !model?.forks || typeof model.forks.forEach !== 'function') return;
213
+ model.forks.forEach((fork: any) => {
214
+ fork?.setProps?.({ options: cloneOptions(props.options) });
215
+ });
216
+ };
217
+
218
+ type FieldStateEditorValue = {
219
+ fields: string[];
220
+ state?: string;
221
+ selectedOptions?: any[];
222
+ };
223
+
224
+ const FieldStateEditor = ({
225
+ value = { fields: [] } as FieldStateEditorValue,
226
+ onChange,
227
+ includeFormStates = true,
228
+ fieldOptionsGetter = getFormFields,
229
+ }) => {
230
+ const ctx = useFlowContext();
231
+ const t = ctx.model.translate.bind(ctx.model);
232
+
233
+ const fieldOptions = fieldOptionsGetter(ctx);
234
+ const selectedFieldModels = (value.fields || [])
235
+ .map((fieldUid) => fieldOptions.find((option) => option.value === fieldUid)?.model)
236
+ .filter(Boolean);
237
+ const canConfigureLimitOptions =
238
+ selectedFieldModels.length === 1 && selectedFieldModels.every((model: any) => supportsLimitOptions(model));
239
+ const stateOptions = [
240
+ { label: t('Visible'), value: 'visible' },
241
+ { label: t('Hidden'), value: 'hidden' },
242
+ { label: t('Hidden (reserved value)'), value: 'hiddenReservedValue' },
243
+ includeFormStates && { label: t('Required'), value: 'required' },
244
+ includeFormStates && { label: t('Not required'), value: 'notRequired' },
245
+ includeFormStates && { label: t('Disabled'), value: 'disabled' },
246
+ includeFormStates && { label: t('Enabled'), value: 'enabled' },
247
+ includeFormStates && canConfigureLimitOptions && { label: t('Options'), value: 'limitOptions' },
248
+ ].filter(Boolean);
249
+ const selectableOptions = getFieldOptionsBySelectedFields(fieldOptions, value.fields, t);
250
+
251
+ const handleFieldsChange = (selectedFields: string[]) => {
252
+ const nextSelectedFieldModels = selectedFields
253
+ .map((fieldUid) => fieldOptions.find((option) => option.value === fieldUid)?.model)
254
+ .filter(Boolean);
255
+ const nextCanConfigureLimitOptions =
256
+ nextSelectedFieldModels.length === 1 &&
257
+ nextSelectedFieldModels.every((model: any) => supportsLimitOptions(model));
258
+ const nextState = value.state === 'limitOptions' && !nextCanConfigureLimitOptions ? undefined : value.state;
259
+ const nextSelectedOptions =
260
+ value.state === 'limitOptions' || nextState === 'limitOptions' ? [] : value.selectedOptions;
261
+ onChange({
262
+ ...value,
263
+ fields: selectedFields,
264
+ state: nextState,
265
+ selectedOptions: nextSelectedOptions,
266
+ });
267
+ };
268
+
269
+ const handleStateChange = (selectedState: string) => {
270
+ onChange({
271
+ ...value,
272
+ state: selectedState,
273
+ selectedOptions: selectedState === 'limitOptions' ? value.selectedOptions || [] : undefined,
274
+ });
275
+ };
276
+
277
+ const handleSelectedOptionsChange = (selectedValues: any[]) => {
278
+ onChange({
279
+ ...value,
280
+ selectedOptions: selectableOptions.filter((option) => selectedValues.includes(option.value)),
281
+ });
282
+ };
283
+
284
+ return (
285
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
286
+ <div>
287
+ <div style={{ marginBottom: '4px', fontSize: '14px' }}>{t('Fields')}</div>
288
+ <Select
289
+ mode="multiple"
290
+ value={value.fields}
291
+ onChange={handleFieldsChange}
292
+ placeholder={t('Please select fields')}
293
+ style={{ width: '100%' }}
294
+ options={fieldOptions}
295
+ showSearch
296
+ // @ts-ignore
297
+ filterOption={(input, option) => (option?.label ?? '').toLowerCase().includes(input.toLowerCase())}
298
+ allowClear
299
+ />
300
+ </div>
301
+ <div>
302
+ <div style={{ marginBottom: '4px', fontSize: '14px' }}>{t('State')}</div>
303
+ <Select
304
+ value={value.state}
305
+ onChange={handleStateChange}
306
+ placeholder={t('Please select state')}
307
+ style={{ width: '100%' }}
308
+ options={stateOptions}
309
+ allowClear
310
+ />
311
+ </div>
312
+ {value.state === 'limitOptions' && canConfigureLimitOptions && (
313
+ <div>
314
+ <div style={{ marginBottom: '4px', fontSize: '14px' }}>{t('Options')}</div>
315
+ <Select
316
+ mode="multiple"
317
+ value={(value.selectedOptions || []).map((option: any) => option.value)}
318
+ onChange={handleSelectedOptionsChange}
319
+ style={{ width: '100%' }}
320
+ options={selectableOptions}
321
+ showSearch
322
+ // @ts-ignore
323
+ filterOption={(input, option) => (option?.label ?? '').toLowerCase().includes(input.toLowerCase())}
324
+ allowClear
325
+ />
326
+ </div>
327
+ )}
328
+ </div>
329
+ );
330
+ };
331
+
123
332
  const getFormFieldsByForkModel = (ctx: any) => {
124
333
  try {
125
334
  const fieldModels = ctx.model?.subModels?.grid?.subModels?.items || [];
@@ -479,80 +688,54 @@ export const linkageSetActionProps = defineAction({
479
688
  },
480
689
  });
481
690
 
482
- export const linkageSetFieldProps = defineAction({
483
- name: 'linkageSetFieldProps',
484
- title: tExpr('Set field state'),
485
- scene: ActionScene.FIELD_LINKAGE_RULES,
691
+ export const linkageSetMenuItemProps = defineAction({
692
+ name: 'linkageSetMenuItemProps',
693
+ title: tExpr('Set menu item state'),
694
+ scene: ActionScene.MENU_LINKAGE_RULES,
486
695
  sort: 100,
487
696
  uiSchema: {
488
697
  value: {
489
- type: 'object',
698
+ type: 'string',
490
699
  'x-component': (props) => {
491
- const { value = { fields: [] }, onChange } = props;
700
+ const { value, onChange } = props;
492
701
  // eslint-disable-next-line react-hooks/rules-of-hooks
493
702
  const ctx = useFlowContext();
494
703
  const t = ctx.model.translate.bind(ctx.model);
495
704
 
496
- const fieldOptions = getFormFields(ctx);
497
-
498
- // 状态选项
499
- const stateOptions = [
500
- { label: t('Visible'), value: 'visible' },
501
- { label: t('Hidden'), value: 'hidden' },
502
- { label: t('Hidden (reserved value)'), value: 'hiddenReservedValue' },
503
- { label: t('Required'), value: 'required' },
504
- { label: t('Not required'), value: 'notRequired' },
505
- { label: t('Disabled'), value: 'disabled' },
506
- { label: t('Enabled'), value: 'enabled' },
507
- ];
508
-
509
- const handleFieldsChange = (selectedFields: string[]) => {
510
- onChange({
511
- ...value,
512
- fields: selectedFields,
513
- });
514
- };
515
-
516
- const handleStateChange = (selectedState: string) => {
517
- onChange({
518
- ...value,
519
- state: selectedState,
520
- });
521
- };
522
-
523
705
  return (
524
- <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
525
- <div>
526
- <div style={{ marginBottom: '4px', fontSize: '14px' }}>{t('Fields')}</div>
527
- <Select
528
- mode="multiple"
529
- value={value.fields}
530
- onChange={handleFieldsChange}
531
- placeholder={t('Please select fields')}
532
- style={{ width: '100%' }}
533
- options={fieldOptions}
534
- showSearch
535
- // @ts-ignore
536
- filterOption={(input, option) => (option?.label ?? '').toLowerCase().includes(input.toLowerCase())}
537
- allowClear
538
- />
539
- </div>
540
- <div>
541
- <div style={{ marginBottom: '4px', fontSize: '14px' }}>{t('State')}</div>
542
- <Select
543
- value={value.state}
544
- onChange={handleStateChange}
545
- placeholder={t('Please select state')}
546
- style={{ width: '100%' }}
547
- options={stateOptions}
548
- allowClear
549
- />
550
- </div>
551
- </div>
706
+ <Select
707
+ value={value}
708
+ onChange={onChange}
709
+ placeholder={t('Please select state')}
710
+ style={{ width: '100%' }}
711
+ options={[
712
+ { label: t('Visible'), value: 'visible' },
713
+ { label: t('Hidden'), value: 'hidden' },
714
+ ]}
715
+ allowClear
716
+ />
552
717
  );
553
718
  },
554
719
  },
555
720
  },
721
+ handler(ctx, { value, setProps }) {
722
+ setProps(ctx.model, { hiddenModel: value === 'hidden' });
723
+ },
724
+ });
725
+
726
+ export const linkageSetFieldProps = defineAction({
727
+ name: 'linkageSetFieldProps',
728
+ title: tExpr('Set field state'),
729
+ scene: ActionScene.FIELD_LINKAGE_RULES,
730
+ sort: 100,
731
+ uiSchema: {
732
+ value: {
733
+ type: 'object',
734
+ 'x-component': (props) => {
735
+ return <FieldStateEditor {...props} />;
736
+ },
737
+ },
738
+ },
556
739
  handler: (ctx, { value, setProps }) => {
557
740
  const { fields, state } = value || {};
558
741
 
@@ -567,36 +750,13 @@ export const linkageSetFieldProps = defineAction({
567
750
  const fieldModel = gridModels.find((model: any) => model.uid === fieldUid);
568
751
 
569
752
  if (fieldModel) {
570
- let props: any = {};
571
-
572
- switch (state) {
573
- case 'visible':
574
- props = { hiddenModel: false };
575
- break;
576
- case 'hidden':
577
- props = { hiddenModel: true };
578
- break;
579
- case 'hiddenReservedValue':
580
- props = { hidden: true };
581
- break;
582
- case 'required':
583
- props = { required: true };
584
- break;
585
- case 'notRequired':
586
- props = { required: false };
587
- break;
588
- case 'disabled':
589
- props = { disabled: true };
590
- break;
591
- case 'enabled':
592
- props = { disabled: false };
593
- break;
594
- default:
595
- console.warn(`Unknown state: ${state}`);
596
- return;
753
+ const props = getFieldStateProps(state, value?.selectedOptions);
754
+ if (!props) {
755
+ console.warn(`Unknown state: ${state}`);
756
+ return;
597
757
  }
598
758
 
599
- setProps(fieldModel as FlowModel, props);
759
+ setProps(getFieldStateTargetModel(state, fieldModel) as FlowModel, props);
600
760
  }
601
761
  } catch (error) {
602
762
  console.warn(`Failed to set props for field ${fieldUid}:`, error);
@@ -614,68 +774,7 @@ export const subFormLinkageSetFieldProps = defineAction({
614
774
  value: {
615
775
  type: 'object',
616
776
  'x-component': (props) => {
617
- const { value = { fields: [] }, onChange } = props;
618
- // eslint-disable-next-line react-hooks/rules-of-hooks
619
- const ctx = useFlowContext();
620
- const t = ctx.model.translate.bind(ctx.model);
621
-
622
- const fieldOptions = getFormFieldsByForkModel(ctx);
623
-
624
- // 状态选项
625
- const stateOptions = [
626
- { label: t('Visible'), value: 'visible' },
627
- { label: t('Hidden'), value: 'hidden' },
628
- { label: t('Hidden (reserved value)'), value: 'hiddenReservedValue' },
629
- { label: t('Required'), value: 'required' },
630
- { label: t('Not required'), value: 'notRequired' },
631
- { label: t('Disabled'), value: 'disabled' },
632
- { label: t('Enabled'), value: 'enabled' },
633
- ];
634
-
635
- const handleFieldsChange = (selectedFields: string[]) => {
636
- onChange({
637
- ...value,
638
- fields: selectedFields,
639
- });
640
- };
641
-
642
- const handleStateChange = (selectedState: string) => {
643
- onChange({
644
- ...value,
645
- state: selectedState,
646
- });
647
- };
648
-
649
- return (
650
- <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
651
- <div>
652
- <div style={{ marginBottom: '4px', fontSize: '14px' }}>{t('Fields')}</div>
653
- <Select
654
- mode="multiple"
655
- value={value.fields}
656
- onChange={handleFieldsChange}
657
- placeholder={t('Please select fields')}
658
- style={{ width: '100%' }}
659
- options={fieldOptions}
660
- showSearch
661
- // @ts-ignore
662
- filterOption={(input, option) => (option?.label ?? '').toLowerCase().includes(input.toLowerCase())}
663
- allowClear
664
- />
665
- </div>
666
- <div>
667
- <div style={{ marginBottom: '4px', fontSize: '14px' }}>{t('State')}</div>
668
- <Select
669
- value={value.state}
670
- onChange={handleStateChange}
671
- placeholder={t('Please select state')}
672
- style={{ width: '100%' }}
673
- options={stateOptions}
674
- allowClear
675
- />
676
- </div>
677
- </div>
678
- );
777
+ return <FieldStateEditor {...props} fieldOptionsGetter={getFormFieldsByForkModel} />;
679
778
  },
680
779
  },
681
780
  },
@@ -713,36 +812,13 @@ export const subFormLinkageSetFieldProps = defineAction({
713
812
  }
714
813
 
715
814
  if (model) {
716
- let props: any = {};
717
-
718
- switch (state) {
719
- case 'visible':
720
- props = { hiddenModel: false };
721
- break;
722
- case 'hidden':
723
- props = { hiddenModel: true };
724
- break;
725
- case 'hiddenReservedValue':
726
- props = { hidden: true };
727
- break;
728
- case 'required':
729
- props = { required: true };
730
- break;
731
- case 'notRequired':
732
- props = { required: false };
733
- break;
734
- case 'disabled':
735
- props = { disabled: true };
736
- break;
737
- case 'enabled':
738
- props = { disabled: false };
739
- break;
740
- default:
741
- console.warn(`Unknown state: ${state}`);
742
- return;
815
+ const props = getFieldStateProps(state, value?.selectedOptions);
816
+ if (!props) {
817
+ console.warn(`Unknown state: ${state}`);
818
+ return;
743
819
  }
744
820
 
745
- setProps(model as FlowModel, props);
821
+ setProps(getFieldStateTargetModel(state, model) as FlowModel, props);
746
822
  }
747
823
  } catch (error) {
748
824
  console.warn(`Failed to set props for field ${fieldUid}:`, error);
@@ -760,64 +836,7 @@ export const linkageSetDetailsFieldProps = defineAction({
760
836
  value: {
761
837
  type: 'object',
762
838
  'x-component': (props) => {
763
- const { value = { fields: [] }, onChange } = props;
764
- // eslint-disable-next-line react-hooks/rules-of-hooks
765
- const ctx = useFlowContext();
766
- const t = ctx.model.translate.bind(ctx.model);
767
-
768
- const fieldOptions = getFormFields(ctx);
769
-
770
- // 状态选项
771
- const stateOptions = [
772
- { label: t('Visible'), value: 'visible' },
773
- { label: t('Hidden'), value: 'hidden' },
774
- { label: t('Hidden (reserved value)'), value: 'hiddenReservedValue' },
775
- ];
776
-
777
- const handleFieldsChange = (selectedFields: string[]) => {
778
- onChange({
779
- ...value,
780
- fields: selectedFields,
781
- });
782
- };
783
-
784
- const handleStateChange = (selectedState: string) => {
785
- onChange({
786
- ...value,
787
- state: selectedState,
788
- });
789
- };
790
-
791
- return (
792
- <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
793
- <div>
794
- <div style={{ marginBottom: '4px', fontSize: '14px' }}>{t('Fields')}</div>
795
- <Select
796
- mode="multiple"
797
- value={value.fields}
798
- onChange={handleFieldsChange}
799
- placeholder={t('Please select fields')}
800
- style={{ width: '100%' }}
801
- options={fieldOptions}
802
- showSearch
803
- // @ts-ignore
804
- filterOption={(input, option) => (option?.label ?? '').toLowerCase().includes(input.toLowerCase())}
805
- allowClear
806
- />
807
- </div>
808
- <div>
809
- <div style={{ marginBottom: '4px', fontSize: '14px' }}>{t('State')}</div>
810
- <Select
811
- value={value.state}
812
- onChange={handleStateChange}
813
- placeholder={t('Please select state')}
814
- style={{ width: '100%' }}
815
- options={stateOptions}
816
- allowClear
817
- />
818
- </div>
819
- </div>
820
- );
839
+ return <FieldStateEditor {...props} includeFormStates={false} />;
821
840
  },
822
841
  },
823
842
  },
@@ -835,24 +854,13 @@ export const linkageSetDetailsFieldProps = defineAction({
835
854
  const fieldModel = gridModels.find((model: any) => model.uid === fieldUid);
836
855
 
837
856
  if (fieldModel) {
838
- let props: any = {};
839
-
840
- switch (state) {
841
- case 'visible':
842
- props = { hiddenModel: false };
843
- break;
844
- case 'hidden':
845
- props = { hiddenModel: true };
846
- break;
847
- case 'hiddenReservedValue':
848
- props = { hidden: true };
849
- break;
850
- default:
851
- console.warn(`Unknown state: ${state}`);
852
- return;
857
+ const props = getFieldStateProps(state);
858
+ if (!props) {
859
+ console.warn(`Unknown state: ${state}`);
860
+ return;
853
861
  }
854
862
 
855
- setProps(fieldModel as FlowModel, props);
863
+ setProps(getFieldStateTargetModel(state, fieldModel) as FlowModel, props);
856
864
  }
857
865
  } catch (error) {
858
866
  console.warn(`Failed to set props for field ${fieldUid}:`, error);
@@ -1288,6 +1296,7 @@ export const linkageRunjs = defineAction({
1288
1296
  ActionScene.BLOCK_LINKAGE_RULES,
1289
1297
  ActionScene.FIELD_LINKAGE_RULES,
1290
1298
  ActionScene.ACTION_LINKAGE_RULES,
1299
+ ActionScene.MENU_LINKAGE_RULES,
1291
1300
  ActionScene.DETAILS_FIELD_LINKAGE_RULES,
1292
1301
  ActionScene.SUB_FORM_FIELD_LINKAGE_RULES,
1293
1302
  ],
@@ -1755,6 +1764,8 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
1755
1764
 
1756
1765
  const linkageRules: LinkageRule[] = params.value as LinkageRule[];
1757
1766
  const allModels: FlowModel[] = ctx.model.__allModels || (ctx.model.__allModels = []);
1767
+ const modelsToApply = new Set<FlowModel>(allModels);
1768
+ const patchPropsByModel = new Map<FlowModel, any>();
1758
1769
  const directValuePatches: Array<{ path: Array<string | number>; value: any; whenEmpty?: boolean }> = [];
1759
1770
  const rootCollection = getCollectionFromModel((ctx.model as any)?.context?.blockModel ?? ctx.model);
1760
1771
  const isSafeToWriteAssociationSubpath = (namePath: any): boolean => {
@@ -1906,11 +1917,6 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
1906
1917
  return null;
1907
1918
  };
1908
1919
 
1909
- allModels.forEach((model: any) => {
1910
- // 重置临时属性
1911
- model.__props = {};
1912
- });
1913
-
1914
1920
  // 1. 运行所有的联动规则
1915
1921
  for (const rule of linkageRules.filter((r) => r.enable)) {
1916
1922
  const { condition: conditions, actions } = rule;
@@ -1919,10 +1925,7 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
1919
1925
  if (!matched) continue;
1920
1926
 
1921
1927
  for (const action of actions) {
1922
- const setProps = (
1923
- model: FlowModel & { __originalProps?: any; __props?: any; __shouldReset?: boolean },
1924
- props: any,
1925
- ) => {
1928
+ const setProps = (model: FlowModel & { __originalProps?: any; __shouldReset?: boolean }, props: any) => {
1926
1929
  // 存储原始值,用于恢复
1927
1930
  if (!model.__originalProps) {
1928
1931
  model.__originalProps = {
@@ -1935,19 +1938,16 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
1935
1938
  };
1936
1939
  }
1937
1940
 
1938
- if (!model.__props) {
1939
- model.__props = {};
1940
- }
1941
-
1942
1941
  // 临时存起来,遍历完所有规则后,再统一处理
1943
- model.__props = {
1944
- ...model.__props,
1942
+ patchPropsByModel.set(model, {
1943
+ ...(patchPropsByModel.get(model) || {}),
1945
1944
  ...props,
1946
- };
1945
+ });
1947
1946
 
1948
1947
  if (allModels.indexOf(model) === -1) {
1949
1948
  allModels.push(model);
1950
1949
  }
1950
+ modelsToApply.add(model);
1951
1951
  };
1952
1952
 
1953
1953
  // TODO: 需要改成 runAction 的写法。但 runAction 是异步的,用在这里会不符合预期。后面需要解决这个问题
@@ -1956,23 +1956,26 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
1956
1956
  }
1957
1957
 
1958
1958
  // 2. 合并去重(按 uid)后再实际更改相关 model 的状态,避免重复项把“已设置的临时属性”覆盖掉
1959
- const mergedByUid = new Map<
1960
- string,
1961
- FlowModel & { __originalProps?: any; __props?: any; isFork?: boolean; forkId?: number }
1962
- >();
1959
+ const mergedByUid = new Map<string, FlowModel & { __originalProps?: any; isFork?: boolean; forkId?: number }>();
1963
1960
  const mergedPropsByUid = new Map<string, any>();
1964
1961
 
1965
- allModels.forEach((m: any) => {
1962
+ modelsToApply.forEach((m: any) => {
1966
1963
  const uid = m?.uid || String(m);
1967
- const curProps = m.__props || {};
1964
+ const curProps = patchPropsByModel.get(m) || {};
1968
1965
  if (!mergedByUid.has(uid)) {
1969
1966
  mergedByUid.set(uid, m);
1970
1967
  mergedPropsByUid.set(uid, { ...curProps });
1971
1968
  } else {
1972
1969
  // 合并属性:后写覆盖先写;优先选择 fork 模型作为应用目标
1973
- mergedPropsByUid.set(uid, { ...mergedPropsByUid.get(uid), ...curProps });
1970
+ const prevProps = mergedPropsByUid.get(uid) || {};
1971
+ const nextProps = { ...prevProps, ...curProps };
1972
+ mergedPropsByUid.set(uid, nextProps);
1974
1973
  const exist = mergedByUid.get(uid) as any;
1975
- if (m.isFork && !exist.isFork) {
1974
+ const hasCurrentPatch = Object.keys(curProps).length > 0;
1975
+ const hasExistingPatch = Object.keys(prevProps).length > 0;
1976
+ if (hasCurrentPatch && (!hasExistingPatch || !m.isFork || !exist.isFork)) {
1977
+ mergedByUid.set(uid, m);
1978
+ } else if (m.isFork && !exist.isFork) {
1976
1979
  mergedByUid.set(uid, m);
1977
1980
  }
1978
1981
  }
@@ -1983,7 +1986,12 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
1983
1986
  const newProps = { ...model.__originalProps, ...patchProps };
1984
1987
 
1985
1988
  model.setProps(_.omit(newProps, ['hiddenModel', 'value', 'hiddenText']));
1986
- model.hidden = !!newProps.hiddenModel;
1989
+ syncFieldOptionsToForks(model, patchProps);
1990
+ if (typeof model.setHidden === 'function') {
1991
+ model.setHidden(!!newProps.hiddenModel);
1992
+ } else {
1993
+ model.hidden = !!newProps.hiddenModel;
1994
+ }
1987
1995
 
1988
1996
  if (newProps.required === true) {
1989
1997
  const rules = (model.props.rules || []).filter((rule) => !rule.required);
@@ -2001,8 +2009,10 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
2001
2009
  model.setProps('title', '');
2002
2010
  }
2003
2011
 
2004
- // 目前只有表单的“字段赋值”有 value 属性
2005
- if ('value' in newProps && model.context.form) {
2012
+ // 只有本轮联动动作显式写入 value 时,才应执行表单赋值。
2013
+ // 普通字段组件也会由 Form.Item/FieldModelRenderer 注入 value;如果从 __originalProps 继承该值,
2014
+ // limitOptions/disabled 等状态联动会误把当前表单值清空。
2015
+ if (Object.prototype.hasOwnProperty.call(patchProps, 'value') && model.context.form) {
2006
2016
  const targetPath = getModelTargetPathForPatch(model);
2007
2017
  if (!targetPath) {
2008
2018
  console.warn('[linkageRules] Skip linkage assignment due to missing target path', {
@@ -2149,6 +2159,32 @@ export const actionLinkageRules = defineAction({
2149
2159
  },
2150
2160
  });
2151
2161
 
2162
+ export const menuLinkageRules = defineAction({
2163
+ name: 'menuLinkageRules',
2164
+ title: tExpr('Menu linkage rules'),
2165
+ uiMode: 'embed',
2166
+ uiSchema(ctx) {
2167
+ return {
2168
+ value: {
2169
+ type: 'array',
2170
+ 'x-component': LinkageRulesUI,
2171
+ 'x-component-props': {
2172
+ supportedActions: getSupportedActions(ctx, ActionScene.MENU_LINKAGE_RULES),
2173
+ title: tExpr('Menu linkage rules'),
2174
+ },
2175
+ },
2176
+ };
2177
+ },
2178
+ defaultParams: {
2179
+ value: [],
2180
+ },
2181
+ useRawParams: true,
2182
+ handler: async (ctx, params) => {
2183
+ const resolved = await resolveLinkageRulesParamsPreservingRunJsScripts(ctx, params);
2184
+ return commonLinkageRulesHandler(ctx, resolved);
2185
+ },
2186
+ });
2187
+
2152
2188
  export const fieldLinkageRules = defineAction({
2153
2189
  name: 'fieldLinkageRules',
2154
2190
  title: tExpr('Field linkage rules'),