@nocobase/client-v2 2.1.0-beta.30 → 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 (72) 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/internal/utils/enumOptionsUtils.d.ts +5 -0
  11. package/es/flow/models/actions/AssociationActionUtils.d.ts +5 -0
  12. package/es/flow/models/blocks/filter-form/FilterFormBlockModel.d.ts +4 -0
  13. package/es/flow/models/blocks/filter-manager/FilterManager.d.ts +5 -1
  14. package/es/flow/models/blocks/table/TableSelectModel.d.ts +8 -0
  15. package/es/flow/models/fields/DisplayTitleFieldModel.d.ts +1 -1
  16. package/es/flow/models/utils/displayValueUtils.d.ts +12 -0
  17. package/es/flow-compat/passwordUtils.d.ts +1 -1
  18. package/es/index.mjs +87 -76
  19. package/es/utils/remotePlugins.d.ts +0 -4
  20. package/lib/index.js +97 -86
  21. package/package.json +6 -5
  22. package/src/BaseApplication.tsx +14 -8
  23. package/src/PluginManager.ts +1 -0
  24. package/src/__tests__/app.test.tsx +28 -1
  25. package/src/__tests__/remotePlugins.test.ts +29 -18
  26. package/src/components/form/DrawerFormLayout.tsx +103 -0
  27. package/src/components/form/EnvVariableInput.tsx +126 -0
  28. package/src/components/form/FileSizeInput.tsx +105 -0
  29. package/src/components/form/createFormRegistry.ts +60 -0
  30. package/src/components/form/index.tsx +14 -0
  31. package/src/components/index.ts +1 -1
  32. package/src/flow/actions/__tests__/dataScopeFilter.test.ts +92 -13
  33. package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +476 -1
  34. package/src/flow/actions/linkageRules.tsx +240 -258
  35. package/src/flow/actions/setTargetDataScope.tsx +32 -3
  36. package/src/flow/components/FieldAssignRulesEditor.tsx +2 -0
  37. package/src/flow/components/__tests__/FieldAssignRulesEditor.test.tsx +81 -4
  38. package/src/flow/components/filter/LinkageFilterItem.tsx +9 -2
  39. package/src/flow/components/filter/VariableFilterItem.tsx +2 -6
  40. package/src/flow/components/filter/__tests__/LinkageFilterItem.test.tsx +71 -0
  41. package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +48 -0
  42. package/src/flow/internal/utils/__tests__/enumOptionsUtils.test.ts +10 -1
  43. package/src/flow/internal/utils/enumOptionsUtils.ts +29 -0
  44. package/src/flow/models/actions/AssociateActionModel.tsx +2 -2
  45. package/src/flow/models/actions/AssociationActionUtils.ts +14 -0
  46. package/src/flow/models/actions/__tests__/AssociationActionModel.test.ts +63 -0
  47. package/src/flow/models/base/CollectionBlockModel.tsx +7 -0
  48. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +33 -9
  49. package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +53 -13
  50. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.getFilterValue.test.ts +63 -3
  51. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +33 -1
  52. package/src/flow/models/blocks/filter-manager/FilterManager.ts +66 -2
  53. package/src/flow/models/blocks/filter-manager/__tests__/FilterManager.test.ts +270 -0
  54. package/src/flow/models/blocks/form/FormBlockModel.tsx +8 -5
  55. package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +30 -0
  56. package/src/flow/models/blocks/form/value-runtime/rules.ts +6 -1
  57. package/src/flow/models/blocks/form/value-runtime/runtime.ts +6 -1
  58. package/src/flow/models/blocks/table/TableBlockModel.tsx +11 -6
  59. package/src/flow/models/blocks/table/TableColumnModel.tsx +3 -0
  60. package/src/flow/models/blocks/table/TableSelectModel.tsx +36 -26
  61. package/src/flow/models/blocks/table/__tests__/TableBlockModel.rowClick.test.ts +69 -0
  62. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +96 -1
  63. package/src/flow/models/blocks/table/__tests__/TableSelectModel.test.ts +41 -0
  64. package/src/flow/models/fields/ClickableFieldModel.tsx +9 -4
  65. package/src/flow/models/fields/DisplayTitleFieldModel.tsx +12 -4
  66. package/src/flow/models/fields/SelectFieldModel.tsx +31 -1
  67. package/src/flow/models/fields/__tests__/ClickableFieldModel.test.ts +23 -0
  68. package/src/flow/models/fields/mobile-components/MobileSelect.tsx +2 -1
  69. package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +7 -0
  70. package/src/flow/models/utils/displayValueUtils.ts +57 -0
  71. package/src/utils/globalDeps.ts +2 -0
  72. 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 || [];
@@ -523,68 +732,7 @@ export const linkageSetFieldProps = defineAction({
523
732
  value: {
524
733
  type: 'object',
525
734
  'x-component': (props) => {
526
- const { value = { fields: [] }, onChange } = props;
527
- // eslint-disable-next-line react-hooks/rules-of-hooks
528
- const ctx = useFlowContext();
529
- const t = ctx.model.translate.bind(ctx.model);
530
-
531
- const fieldOptions = getFormFields(ctx);
532
-
533
- // 状态选项
534
- const stateOptions = [
535
- { label: t('Visible'), value: 'visible' },
536
- { label: t('Hidden'), value: 'hidden' },
537
- { label: t('Hidden (reserved value)'), value: 'hiddenReservedValue' },
538
- { label: t('Required'), value: 'required' },
539
- { label: t('Not required'), value: 'notRequired' },
540
- { label: t('Disabled'), value: 'disabled' },
541
- { label: t('Enabled'), value: 'enabled' },
542
- ];
543
-
544
- const handleFieldsChange = (selectedFields: string[]) => {
545
- onChange({
546
- ...value,
547
- fields: selectedFields,
548
- });
549
- };
550
-
551
- const handleStateChange = (selectedState: string) => {
552
- onChange({
553
- ...value,
554
- state: selectedState,
555
- });
556
- };
557
-
558
- return (
559
- <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
560
- <div>
561
- <div style={{ marginBottom: '4px', fontSize: '14px' }}>{t('Fields')}</div>
562
- <Select
563
- mode="multiple"
564
- value={value.fields}
565
- onChange={handleFieldsChange}
566
- placeholder={t('Please select fields')}
567
- style={{ width: '100%' }}
568
- options={fieldOptions}
569
- showSearch
570
- // @ts-ignore
571
- filterOption={(input, option) => (option?.label ?? '').toLowerCase().includes(input.toLowerCase())}
572
- allowClear
573
- />
574
- </div>
575
- <div>
576
- <div style={{ marginBottom: '4px', fontSize: '14px' }}>{t('State')}</div>
577
- <Select
578
- value={value.state}
579
- onChange={handleStateChange}
580
- placeholder={t('Please select state')}
581
- style={{ width: '100%' }}
582
- options={stateOptions}
583
- allowClear
584
- />
585
- </div>
586
- </div>
587
- );
735
+ return <FieldStateEditor {...props} />;
588
736
  },
589
737
  },
590
738
  },
@@ -602,36 +750,13 @@ export const linkageSetFieldProps = defineAction({
602
750
  const fieldModel = gridModels.find((model: any) => model.uid === fieldUid);
603
751
 
604
752
  if (fieldModel) {
605
- let props: any = {};
606
-
607
- switch (state) {
608
- case 'visible':
609
- props = { hiddenModel: false };
610
- break;
611
- case 'hidden':
612
- props = { hiddenModel: true };
613
- break;
614
- case 'hiddenReservedValue':
615
- props = { hidden: true };
616
- break;
617
- case 'required':
618
- props = { required: true };
619
- break;
620
- case 'notRequired':
621
- props = { required: false };
622
- break;
623
- case 'disabled':
624
- props = { disabled: true };
625
- break;
626
- case 'enabled':
627
- props = { disabled: false };
628
- break;
629
- default:
630
- console.warn(`Unknown state: ${state}`);
631
- return;
753
+ const props = getFieldStateProps(state, value?.selectedOptions);
754
+ if (!props) {
755
+ console.warn(`Unknown state: ${state}`);
756
+ return;
632
757
  }
633
758
 
634
- setProps(fieldModel as FlowModel, props);
759
+ setProps(getFieldStateTargetModel(state, fieldModel) as FlowModel, props);
635
760
  }
636
761
  } catch (error) {
637
762
  console.warn(`Failed to set props for field ${fieldUid}:`, error);
@@ -649,68 +774,7 @@ export const subFormLinkageSetFieldProps = defineAction({
649
774
  value: {
650
775
  type: 'object',
651
776
  'x-component': (props) => {
652
- const { value = { fields: [] }, onChange } = props;
653
- // eslint-disable-next-line react-hooks/rules-of-hooks
654
- const ctx = useFlowContext();
655
- const t = ctx.model.translate.bind(ctx.model);
656
-
657
- const fieldOptions = getFormFieldsByForkModel(ctx);
658
-
659
- // 状态选项
660
- const stateOptions = [
661
- { label: t('Visible'), value: 'visible' },
662
- { label: t('Hidden'), value: 'hidden' },
663
- { label: t('Hidden (reserved value)'), value: 'hiddenReservedValue' },
664
- { label: t('Required'), value: 'required' },
665
- { label: t('Not required'), value: 'notRequired' },
666
- { label: t('Disabled'), value: 'disabled' },
667
- { label: t('Enabled'), value: 'enabled' },
668
- ];
669
-
670
- const handleFieldsChange = (selectedFields: string[]) => {
671
- onChange({
672
- ...value,
673
- fields: selectedFields,
674
- });
675
- };
676
-
677
- const handleStateChange = (selectedState: string) => {
678
- onChange({
679
- ...value,
680
- state: selectedState,
681
- });
682
- };
683
-
684
- return (
685
- <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
686
- <div>
687
- <div style={{ marginBottom: '4px', fontSize: '14px' }}>{t('Fields')}</div>
688
- <Select
689
- mode="multiple"
690
- value={value.fields}
691
- onChange={handleFieldsChange}
692
- placeholder={t('Please select fields')}
693
- style={{ width: '100%' }}
694
- options={fieldOptions}
695
- showSearch
696
- // @ts-ignore
697
- filterOption={(input, option) => (option?.label ?? '').toLowerCase().includes(input.toLowerCase())}
698
- allowClear
699
- />
700
- </div>
701
- <div>
702
- <div style={{ marginBottom: '4px', fontSize: '14px' }}>{t('State')}</div>
703
- <Select
704
- value={value.state}
705
- onChange={handleStateChange}
706
- placeholder={t('Please select state')}
707
- style={{ width: '100%' }}
708
- options={stateOptions}
709
- allowClear
710
- />
711
- </div>
712
- </div>
713
- );
777
+ return <FieldStateEditor {...props} fieldOptionsGetter={getFormFieldsByForkModel} />;
714
778
  },
715
779
  },
716
780
  },
@@ -748,36 +812,13 @@ export const subFormLinkageSetFieldProps = defineAction({
748
812
  }
749
813
 
750
814
  if (model) {
751
- let props: any = {};
752
-
753
- switch (state) {
754
- case 'visible':
755
- props = { hiddenModel: false };
756
- break;
757
- case 'hidden':
758
- props = { hiddenModel: true };
759
- break;
760
- case 'hiddenReservedValue':
761
- props = { hidden: true };
762
- break;
763
- case 'required':
764
- props = { required: true };
765
- break;
766
- case 'notRequired':
767
- props = { required: false };
768
- break;
769
- case 'disabled':
770
- props = { disabled: true };
771
- break;
772
- case 'enabled':
773
- props = { disabled: false };
774
- break;
775
- default:
776
- console.warn(`Unknown state: ${state}`);
777
- return;
815
+ const props = getFieldStateProps(state, value?.selectedOptions);
816
+ if (!props) {
817
+ console.warn(`Unknown state: ${state}`);
818
+ return;
778
819
  }
779
820
 
780
- setProps(model as FlowModel, props);
821
+ setProps(getFieldStateTargetModel(state, model) as FlowModel, props);
781
822
  }
782
823
  } catch (error) {
783
824
  console.warn(`Failed to set props for field ${fieldUid}:`, error);
@@ -795,64 +836,7 @@ export const linkageSetDetailsFieldProps = defineAction({
795
836
  value: {
796
837
  type: 'object',
797
838
  'x-component': (props) => {
798
- const { value = { fields: [] }, onChange } = props;
799
- // eslint-disable-next-line react-hooks/rules-of-hooks
800
- const ctx = useFlowContext();
801
- const t = ctx.model.translate.bind(ctx.model);
802
-
803
- const fieldOptions = getFormFields(ctx);
804
-
805
- // 状态选项
806
- const stateOptions = [
807
- { label: t('Visible'), value: 'visible' },
808
- { label: t('Hidden'), value: 'hidden' },
809
- { label: t('Hidden (reserved value)'), value: 'hiddenReservedValue' },
810
- ];
811
-
812
- const handleFieldsChange = (selectedFields: string[]) => {
813
- onChange({
814
- ...value,
815
- fields: selectedFields,
816
- });
817
- };
818
-
819
- const handleStateChange = (selectedState: string) => {
820
- onChange({
821
- ...value,
822
- state: selectedState,
823
- });
824
- };
825
-
826
- return (
827
- <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
828
- <div>
829
- <div style={{ marginBottom: '4px', fontSize: '14px' }}>{t('Fields')}</div>
830
- <Select
831
- mode="multiple"
832
- value={value.fields}
833
- onChange={handleFieldsChange}
834
- placeholder={t('Please select fields')}
835
- style={{ width: '100%' }}
836
- options={fieldOptions}
837
- showSearch
838
- // @ts-ignore
839
- filterOption={(input, option) => (option?.label ?? '').toLowerCase().includes(input.toLowerCase())}
840
- allowClear
841
- />
842
- </div>
843
- <div>
844
- <div style={{ marginBottom: '4px', fontSize: '14px' }}>{t('State')}</div>
845
- <Select
846
- value={value.state}
847
- onChange={handleStateChange}
848
- placeholder={t('Please select state')}
849
- style={{ width: '100%' }}
850
- options={stateOptions}
851
- allowClear
852
- />
853
- </div>
854
- </div>
855
- );
839
+ return <FieldStateEditor {...props} includeFormStates={false} />;
856
840
  },
857
841
  },
858
842
  },
@@ -870,24 +854,13 @@ export const linkageSetDetailsFieldProps = defineAction({
870
854
  const fieldModel = gridModels.find((model: any) => model.uid === fieldUid);
871
855
 
872
856
  if (fieldModel) {
873
- let props: any = {};
874
-
875
- switch (state) {
876
- case 'visible':
877
- props = { hiddenModel: false };
878
- break;
879
- case 'hidden':
880
- props = { hiddenModel: true };
881
- break;
882
- case 'hiddenReservedValue':
883
- props = { hidden: true };
884
- break;
885
- default:
886
- console.warn(`Unknown state: ${state}`);
887
- return;
857
+ const props = getFieldStateProps(state);
858
+ if (!props) {
859
+ console.warn(`Unknown state: ${state}`);
860
+ return;
888
861
  }
889
862
 
890
- setProps(fieldModel as FlowModel, props);
863
+ setProps(getFieldStateTargetModel(state, fieldModel) as FlowModel, props);
891
864
  }
892
865
  } catch (error) {
893
866
  console.warn(`Failed to set props for field ${fieldUid}:`, error);
@@ -1994,9 +1967,15 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
1994
1967
  mergedPropsByUid.set(uid, { ...curProps });
1995
1968
  } else {
1996
1969
  // 合并属性:后写覆盖先写;优先选择 fork 模型作为应用目标
1997
- mergedPropsByUid.set(uid, { ...mergedPropsByUid.get(uid), ...curProps });
1970
+ const prevProps = mergedPropsByUid.get(uid) || {};
1971
+ const nextProps = { ...prevProps, ...curProps };
1972
+ mergedPropsByUid.set(uid, nextProps);
1998
1973
  const exist = mergedByUid.get(uid) as any;
1999
- 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) {
2000
1979
  mergedByUid.set(uid, m);
2001
1980
  }
2002
1981
  }
@@ -2007,6 +1986,7 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
2007
1986
  const newProps = { ...model.__originalProps, ...patchProps };
2008
1987
 
2009
1988
  model.setProps(_.omit(newProps, ['hiddenModel', 'value', 'hiddenText']));
1989
+ syncFieldOptionsToForks(model, patchProps);
2010
1990
  if (typeof model.setHidden === 'function') {
2011
1991
  model.setHidden(!!newProps.hiddenModel);
2012
1992
  } else {
@@ -2029,8 +2009,10 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
2029
2009
  model.setProps('title', '');
2030
2010
  }
2031
2011
 
2032
- // 目前只有表单的“字段赋值”有 value 属性
2033
- 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) {
2034
2016
  const targetPath = getModelTargetPathForPatch(model);
2035
2017
  if (!targetPath) {
2036
2018
  console.warn('[linkageRules] Skip linkage assignment due to missing target path', {
@@ -10,6 +10,7 @@
10
10
  import {
11
11
  ActionScene,
12
12
  defineAction,
13
+ extractUsedVariablePaths,
13
14
  FlowModel,
14
15
  MultiRecordResource,
15
16
  useFlowContext,
@@ -20,6 +21,34 @@ import React from 'react';
20
21
  import { FilterGroup, VariableFilterItem } from '../components/filter';
21
22
  import { normalizeDataScopeFilter } from './dataScopeFilter';
22
23
 
24
+ function dependsOnClickedRowRecord(filter: any) {
25
+ if (!filter) {
26
+ return false;
27
+ }
28
+ const used = extractUsedVariablePaths(filter) || {};
29
+ return Object.prototype.hasOwnProperty.call(used, 'clickedRowRecord');
30
+ }
31
+
32
+ function shouldClearClickedRowDataScope(ctx: any, params: any) {
33
+ return ctx.inputArgs?.selected === false && dependsOnClickedRowRecord(params.filter);
34
+ }
35
+
36
+ function resolveTargetDataScopeFilter(ctx: any, params: any, resolvedParams: any) {
37
+ if (shouldClearClickedRowDataScope(ctx, params)) {
38
+ return undefined;
39
+ }
40
+
41
+ return normalizeDataScopeFilter(params.filter, resolvedParams.filter);
42
+ }
43
+
44
+ function shouldRefreshTargetResource(resource: MultiRecordResource) {
45
+ if (resource.hasData()) {
46
+ return true;
47
+ }
48
+
49
+ return resource.getMeta?.('count') !== undefined || resource.getMeta?.('hasNext') !== undefined;
50
+ }
51
+
23
52
  export const setTargetDataScope = defineAction({
24
53
  name: 'setTargetDataScope',
25
54
  title: tExpr('Set data scope'),
@@ -69,20 +98,20 @@ export const setTargetDataScope = defineAction({
69
98
  return;
70
99
  }
71
100
  const model: FlowModel = ctx.model;
101
+ const filter = resolveTargetDataScopeFilter(ctx, params, resolvedParams);
102
+
72
103
  model.scheduleModelOperation(targetBlockUid, (targetModel) => {
73
104
  const resource = targetModel['resource'] as MultiRecordResource;
74
105
  if (!resource) {
75
106
  return;
76
107
  }
77
108
 
78
- const filter = normalizeDataScopeFilter(params.filter, resolvedParams.filter);
79
-
80
109
  if (isEmptyFilter(filter)) {
81
110
  resource.removeFilterGroup(`setTargetDataScope_${ctx.model.uid}`);
82
111
  } else {
83
112
  resource.addFilterGroup(`setTargetDataScope_${ctx.model.uid}`, filter);
84
113
  }
85
- if (resource.hasData()) {
114
+ if (shouldRefreshTargetResource(resource)) {
86
115
  resource.refresh();
87
116
  }
88
117
  });
@@ -35,6 +35,7 @@ type CollectionFieldLike = {
35
35
  title?: unknown;
36
36
  type?: unknown;
37
37
  interface?: unknown;
38
+ uiSchema?: unknown;
38
39
  targetKey?: unknown;
39
40
  targetCollectionTitleFieldName?: unknown;
40
41
  targetCollection?: any;
@@ -194,6 +195,7 @@ export const FieldAssignRulesEditor: React.FC<FieldAssignRulesEditorProps> = (pr
194
195
  title,
195
196
  type: String(f.type || 'string'),
196
197
  interface: fieldInterface,
198
+ uiSchema: (f as any).uiSchema,
197
199
  paths: [...basePaths, name],
198
200
  };
199
201