@nocobase/client-v2 2.2.0-alpha.6 → 2.2.0-alpha.8

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 (49) hide show
  1. package/es/BaseApplication.d.ts +1 -0
  2. package/es/collection-field-interface/CollectionFieldInterface.d.ts +1 -0
  3. package/es/collection-field-interface/CollectionFieldInterfaceManager.d.ts +1 -0
  4. package/es/flow/components/FieldAssignExactDatePicker.d.ts +1 -0
  5. package/es/flow/components/FieldAssignValueInput.d.ts +7 -0
  6. package/es/flow/components/RunJSValueEditor.d.ts +1 -0
  7. package/es/flow/components/filter/FilterGroup.d.ts +1 -0
  8. package/es/flow/components/filter/VariableFilterItem.d.ts +1 -0
  9. package/es/flow/models/blocks/form/QuickEditFormModel.d.ts +17 -2
  10. package/es/index.mjs +89 -89
  11. package/lib/index.js +97 -97
  12. package/package.json +8 -7
  13. package/src/BaseApplication.tsx +11 -6
  14. package/src/__tests__/app.test.tsx +55 -0
  15. package/src/collection-field-interface/CollectionFieldInterface.ts +1 -0
  16. package/src/collection-field-interface/CollectionFieldInterfaceManager.ts +1 -0
  17. package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +145 -2
  18. package/src/components/form/ScanInput/useCodeScanner.ts +154 -2
  19. package/src/components/form/TypedVariableInput.tsx +12 -8
  20. package/src/components/form/__tests__/TypedVariableInput.test.tsx +44 -3
  21. package/src/flow/FlowPage.tsx +9 -1
  22. package/src/flow/__tests__/FlowPage.test.tsx +50 -3
  23. package/src/flow/__tests__/FlowRoute.test.tsx +2 -2
  24. package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +7 -3
  25. package/src/flow/actions/linkageRulesRefresh.tsx +2 -6
  26. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +0 -1
  27. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +253 -6
  28. package/src/flow/components/FieldAssignExactDatePicker.tsx +25 -11
  29. package/src/flow/components/FieldAssignValueInput.tsx +81 -15
  30. package/src/flow/components/FlowRoute.tsx +7 -3
  31. package/src/flow/components/RunJSValueEditor.tsx +9 -1
  32. package/src/flow/components/__tests__/FieldAssignValueInput.context.test.tsx +216 -0
  33. package/src/flow/components/filter/FilterGroup.tsx +33 -5
  34. package/src/flow/components/filter/VariableFilterItem.tsx +139 -6
  35. package/src/flow/components/filter/__tests__/FilterGroup.test.tsx +45 -0
  36. package/src/flow/components/filter/__tests__/VariableFilterItem.leftMetaTree.test.tsx +9 -0
  37. package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +21 -0
  38. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +1 -0
  39. package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +27 -12
  40. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +36 -0
  41. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +34 -0
  42. package/src/flow/models/blocks/form/QuickEditFormModel.tsx +189 -36
  43. package/src/flow/models/blocks/form/__tests__/QuickEditFormModel.quickEdit.test.ts +350 -2
  44. package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +2 -1
  45. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +1 -0
  46. package/src/flow/models/fields/VariableFieldFormModel.tsx +3 -3
  47. package/src/flow/models/fields/__tests__/VariableFieldFormModel.test.tsx +51 -0
  48. package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +20 -10
  49. package/src/flow/models/fields/mobile-components/MobileSelect.tsx +24 -12
@@ -42,6 +42,12 @@ import { normalizeFilterValueByOperator } from '../models/blocks/filter-form/val
42
42
  import { FieldAssignExactDatePicker, type ExactDatePickerMode } from './FieldAssignExactDatePicker';
43
43
  import { limitAssociationMetaTree } from './filter/metaTreeAssociationDepth';
44
44
 
45
+ type VariableInputConverters = {
46
+ renderInputComponent?: (metaTreeNode: MetaTreeNode | null) => React.ComponentType<any> | null;
47
+ resolvePathFromValue?: (value: any) => string[] | undefined;
48
+ resolveValueFromPath?: (metaTreeNode: MetaTreeNode) => any;
49
+ };
50
+
45
51
  const DATE_FIELD_INTERFACES = new Set(['date', 'datetime', 'datetimeNoTz', 'createdAt', 'updatedAt', 'unixTimestamp']);
46
52
 
47
53
  const TZ_AWARE_DATE_INTERFACES = new Set(['datetime', 'createdAt', 'updatedAt', 'unixTimestamp']);
@@ -344,6 +350,8 @@ interface Props {
344
350
  /** 是否允许在变量选择器中使用 RunJS。默认 true,保持历史行为。 */
345
351
  allowRunJS?: boolean;
346
352
  maxAssociationFieldDepth?: number;
353
+ disabled?: boolean;
354
+ variableConverters?: VariableInputConverters;
347
355
  }
348
356
 
349
357
  type ResolvedFieldContext = {
@@ -716,6 +724,8 @@ export const FieldAssignValueInput: React.FC<Props> = ({
716
724
  enableDateVariableAsConstant = false,
717
725
  allowRunJS = true,
718
726
  maxAssociationFieldDepth = 2,
727
+ disabled = false,
728
+ variableConverters,
719
729
  }) => {
720
730
  const flowCtx = useFlowContext<FlowModelContext>();
721
731
  const normalizeEventValue = React.useCallback((eventOrValue: unknown) => {
@@ -1031,18 +1041,22 @@ export const FieldAssignValueInput: React.FC<Props> = ({
1031
1041
  collectionField: effectiveCollectionField,
1032
1042
  });
1033
1043
 
1034
- const created = engine?.createModel?.({
1035
- use: 'VariableFieldFormModel',
1036
- subModels: {
1037
- fields: [
1038
- {
1039
- use: effectiveFieldModelUse,
1040
- stepParams: tempFieldStepParams,
1041
- props: tempFieldProps,
1042
- },
1043
- ],
1044
+ const sourceContext = resolved?.itemModel?.context || flowCtx.model?.context;
1045
+ const created = engine?.createModel?.(
1046
+ {
1047
+ use: 'VariableFieldFormModel',
1048
+ subModels: {
1049
+ fields: [
1050
+ {
1051
+ use: effectiveFieldModelUse,
1052
+ stepParams: tempFieldStepParams,
1053
+ props: tempFieldProps,
1054
+ },
1055
+ ],
1056
+ },
1044
1057
  },
1045
- });
1058
+ sourceContext ? { delegate: sourceContext } : undefined,
1059
+ );
1046
1060
  if (!created) return;
1047
1061
 
1048
1062
  // 注入上下文(集合/数据源/字段/区块/资源)
@@ -1076,7 +1090,7 @@ export const FieldAssignValueInput: React.FC<Props> = ({
1076
1090
  fm.setStepParams('selectSettings', 'fieldNames', { label: overrideLabel });
1077
1091
  }
1078
1092
  fm?.setProps?.({
1079
- disabled: false,
1093
+ disabled,
1080
1094
  readPretty: false,
1081
1095
  pattern: 'editable',
1082
1096
  updateAssociation: false,
@@ -1139,6 +1153,7 @@ export const FieldAssignValueInput: React.FC<Props> = ({
1139
1153
  preferFormItemFieldModel,
1140
1154
  associationFieldNamesOverride?.label,
1141
1155
  associationFieldNamesOverride?.value,
1156
+ disabled,
1142
1157
  ]);
1143
1158
 
1144
1159
  // 当传入 operator / operatorMetaList 时,按 operator schema 适配临时字段的输入组件与 props。
@@ -1191,6 +1206,9 @@ export const FieldAssignValueInput: React.FC<Props> = ({
1191
1206
  React.useEffect(() => {
1192
1207
  const coercedValue = coerceEmptyValueForRenderer(inputProps?.value);
1193
1208
  const handleChange = (ev: any) => {
1209
+ if (inputProps?.disabled) {
1210
+ return;
1211
+ }
1194
1212
  const nextRaw = normalizeEventValue(ev);
1195
1213
  const normalizedForStore = operator ? normalizeFilterValueByOperator(operator, nextRaw) : nextRaw;
1196
1214
  const nextValue = coerceEmptyValueForRenderer(normalizedForStore);
@@ -1222,6 +1240,7 @@ export const FieldAssignValueInput: React.FC<Props> = ({
1222
1240
  value={inputProps?.value}
1223
1241
  onChange={(e) => inputProps?.onChange?.(normalizeEventValue(e))}
1224
1242
  placeholder={placeholder}
1243
+ disabled={inputProps?.disabled}
1225
1244
  style={withFullWidthStyle(wrapperStyle)}
1226
1245
  />
1227
1246
  );
@@ -1240,6 +1259,7 @@ export const FieldAssignValueInput: React.FC<Props> = ({
1240
1259
  const C: React.FC<any> = (inputProps) => {
1241
1260
  const wrapperStyle = pickStyle(inputProps?.style);
1242
1261
  const raw = inputProps?.value;
1262
+ const isDisabled = Boolean(inputProps?.disabled);
1243
1263
  const parsed = isCtxDateExpression(raw) ? parseCtxDateExpression(raw) : raw;
1244
1264
  const parsedValue = typeof parsed === 'undefined' ? undefined : parsed;
1245
1265
  const { token } = theme.useToken();
@@ -1264,6 +1284,9 @@ export const FieldAssignValueInput: React.FC<Props> = ({
1264
1284
  });
1265
1285
 
1266
1286
  const handleSelect = (val: string) => {
1287
+ if (isDisabled) {
1288
+ return;
1289
+ }
1267
1290
  setOpen(false);
1268
1291
  if (val === 'exact') {
1269
1292
  inputProps?.onChange?.('');
@@ -1278,10 +1301,16 @@ export const FieldAssignValueInput: React.FC<Props> = ({
1278
1301
  };
1279
1302
 
1280
1303
  const handleExactSingleChange = (nextValue: any) => {
1304
+ if (isDisabled) {
1305
+ return;
1306
+ }
1281
1307
  inputProps?.onChange?.(nextValue || '');
1282
1308
  };
1283
1309
 
1284
1310
  const handleExactRangeChange = (nextValue: any) => {
1311
+ if (isDisabled) {
1312
+ return;
1313
+ }
1285
1314
  inputProps?.onChange?.(nextValue || '');
1286
1315
  };
1287
1316
 
@@ -1326,7 +1355,11 @@ export const FieldAssignValueInput: React.FC<Props> = ({
1326
1355
  <Select
1327
1356
  options={options}
1328
1357
  open={open}
1329
- onDropdownVisibleChange={setOpen}
1358
+ onDropdownVisibleChange={(nextOpen) => {
1359
+ if (!isDisabled) {
1360
+ setOpen(nextOpen);
1361
+ }
1362
+ }}
1330
1363
  allowClear={false}
1331
1364
  style={{
1332
1365
  width: '100%',
@@ -1336,13 +1369,18 @@ export const FieldAssignValueInput: React.FC<Props> = ({
1336
1369
  value={selectedType}
1337
1370
  onChange={handleSelect}
1338
1371
  dropdownRender={dropdownRender}
1372
+ disabled={isDisabled}
1339
1373
  />
1340
1374
  {['past', 'next'].includes(selectedType) && [
1341
1375
  <InputNumber
1342
1376
  key="number"
1343
1377
  style={{ flex: 1 }}
1344
1378
  value={(parsedValue as any)?.number}
1379
+ disabled={isDisabled}
1345
1380
  onChange={(nextNumber) => {
1381
+ if (isDisabled) {
1382
+ return;
1383
+ }
1346
1384
  inputProps?.onChange?.({
1347
1385
  ...(parsedValue as any),
1348
1386
  type: selectedType,
@@ -1355,7 +1393,11 @@ export const FieldAssignValueInput: React.FC<Props> = ({
1355
1393
  key="unit"
1356
1394
  value={(parsedValue as any)?.unit}
1357
1395
  style={{ minWidth: 130, maxWidth: 140 }}
1396
+ disabled={isDisabled}
1358
1397
  onChange={(nextUnit) => {
1398
+ if (isDisabled) {
1399
+ return;
1400
+ }
1359
1401
  inputProps?.onChange?.({
1360
1402
  ...(parsedValue as any),
1361
1403
  type: selectedType,
@@ -1378,6 +1420,7 @@ export const FieldAssignValueInput: React.FC<Props> = ({
1378
1420
  isRange={isRange}
1379
1421
  value={isRange ? exactRangeValue : exactSingleValue}
1380
1422
  onChange={isRange ? handleExactRangeChange : handleExactSingleChange}
1423
+ disabled={isDisabled}
1381
1424
  style={{ flex: 1 }}
1382
1425
  />
1383
1426
  )}
@@ -1397,7 +1440,12 @@ export const FieldAssignValueInput: React.FC<Props> = ({
1397
1440
 
1398
1441
  const RunJSComponent = React.useMemo(() => {
1399
1442
  const C: React.FC<any> = (inputProps) => (
1400
- <RunJSValueEditor t={flowCtx.t} value={inputProps?.value} onChange={inputProps?.onChange} />
1443
+ <RunJSValueEditor
1444
+ t={flowCtx.t}
1445
+ value={inputProps?.value}
1446
+ onChange={inputProps?.onChange}
1447
+ disabled={inputProps?.disabled}
1448
+ />
1401
1449
  );
1402
1450
  return C;
1403
1451
  }, [flowCtx]);
@@ -1443,6 +1491,10 @@ export const FieldAssignValueInput: React.FC<Props> = ({
1443
1491
 
1444
1492
  const handleVariableInputChange = React.useCallback(
1445
1493
  (nextValue: any) => {
1494
+ if (disabled) {
1495
+ return;
1496
+ }
1497
+
1446
1498
  if (!useDateVariableConstant) {
1447
1499
  onChange(nextValue);
1448
1500
  return;
@@ -1450,7 +1502,7 @@ export const FieldAssignValueInput: React.FC<Props> = ({
1450
1502
 
1451
1503
  onChange(normalizeDateVariableOutput(nextValue, dateVariableComponentProps));
1452
1504
  },
1453
- [dateVariableComponentProps, onChange, useDateVariableConstant],
1505
+ [dateVariableComponentProps, disabled, onChange, useDateVariableConstant],
1454
1506
  );
1455
1507
 
1456
1508
  if (!fieldPath) {
@@ -1465,8 +1517,14 @@ export const FieldAssignValueInput: React.FC<Props> = ({
1465
1517
  metaTree={metaTree}
1466
1518
  style={{ width: '100%' }}
1467
1519
  clearValue={''}
1520
+ disabled={disabled}
1468
1521
  converters={{
1522
+ ...variableConverters,
1469
1523
  renderInputComponent: (meta) => {
1524
+ const external = variableConverters?.renderInputComponent?.(meta ?? null);
1525
+ if (external) {
1526
+ return external;
1527
+ }
1470
1528
  const firstPath = meta?.paths?.[0];
1471
1529
  if (firstPath === 'constant') return ConstantEditor;
1472
1530
  if (firstPath === 'null') return NullComponent;
@@ -1474,6 +1532,10 @@ export const FieldAssignValueInput: React.FC<Props> = ({
1474
1532
  return null;
1475
1533
  },
1476
1534
  resolveValueFromPath: (item) => {
1535
+ const external = variableConverters?.resolveValueFromPath?.(item);
1536
+ if (external !== undefined) {
1537
+ return external;
1538
+ }
1477
1539
  const firstPath = item?.paths?.[0];
1478
1540
  if (firstPath === 'constant') {
1479
1541
  return useDateVariableConstant ? { type: 'today' } : '';
@@ -1483,6 +1545,10 @@ export const FieldAssignValueInput: React.FC<Props> = ({
1483
1545
  return undefined;
1484
1546
  },
1485
1547
  resolvePathFromValue: (currentValue) => {
1548
+ const external = variableConverters?.resolvePathFromValue?.(currentValue);
1549
+ if (external !== undefined) {
1550
+ return external;
1551
+ }
1486
1552
  if (currentValue === null) return ['null'];
1487
1553
  if (allowRunJS && isRunJSValue(currentValue)) return ['runjs'];
1488
1554
  if (useDateVariableConstant && isCtxDateExpression(currentValue)) {
@@ -274,11 +274,12 @@ const FlowRoute = (props: FlowRouteProps = {}) => {
274
274
  useEffect(() => {
275
275
  let active = true;
276
276
  const requestId = ++requestIdRef.current;
277
+ const requiresAccessibleRoute = shouldRequireAccessibleRoute(routeLayout);
277
278
 
278
279
  const run = async () => {
279
280
  setGuardState({ pageUid, pending: true, allowBridge: false, notFound: false });
280
281
 
281
- if (!skipRouteRepositoryCheck && !routeRepository?.isAccessibleLoaded?.()) {
282
+ if (requiresAccessibleRoute && !skipRouteRepositoryCheck && !routeRepository?.isAccessibleLoaded?.()) {
282
283
  try {
283
284
  await routeRepository?.ensureAccessibleLoaded?.();
284
285
  } catch (_error) {
@@ -293,8 +294,11 @@ const FlowRoute = (props: FlowRouteProps = {}) => {
293
294
  return;
294
295
  }
295
296
 
296
- const route = skipRouteRepositoryCheck ? undefined : getAccessibleRouteByPageUid(routeRepository, pageUid);
297
- if (!route && !skipRouteRepositoryCheck && shouldRequireAccessibleRoute(routeLayout)) {
297
+ const route =
298
+ skipRouteRepositoryCheck || !requiresAccessibleRoute
299
+ ? undefined
300
+ : getAccessibleRouteByPageUid(routeRepository, pageUid);
301
+ if (!route && !skipRouteRepositoryCheck && requiresAccessibleRoute) {
298
302
  setGuardState({ pageUid, pending: false, allowBridge: false, notFound: true });
299
303
  return;
300
304
  }
@@ -15,6 +15,7 @@ export interface RunJSValueEditorProps {
15
15
  t?: (key: string) => string;
16
16
  value?: unknown;
17
17
  onChange?: (value: RunJSValue) => void;
18
+ disabled?: boolean;
18
19
  height?: string;
19
20
  scene?: string;
20
21
  containerStyle?: React.CSSProperties;
@@ -25,6 +26,7 @@ export const RunJSValueEditor: React.FC<RunJSValueEditorProps> = (props) => {
25
26
  t,
26
27
  value,
27
28
  onChange,
29
+ disabled,
28
30
  height = '200px',
29
31
  scene = 'formValue',
30
32
  containerStyle = { flex: 1, minWidth: 0 },
@@ -38,9 +40,15 @@ export const RunJSValueEditor: React.FC<RunJSValueEditorProps> = (props) => {
38
40
  <div style={containerStyle}>
39
41
  <CodeEditor
40
42
  value={current.code}
41
- onChange={(code) => onChange?.({ ...current, code })}
43
+ onChange={(code) => {
44
+ if (disabled) {
45
+ return;
46
+ }
47
+ onChange?.({ ...current, code });
48
+ }}
42
49
  version={current.version}
43
50
  height={height}
51
+ readonly={disabled}
44
52
  enableLinter
45
53
  placeholder={placeholderText}
46
54
  scene={scene}
@@ -0,0 +1,216 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ import React from 'react';
11
+ import { describe, expect, it, vi } from 'vitest';
12
+ import { render, waitFor } from '@nocobase/test/client';
13
+ import { FieldAssignValueInput } from '../FieldAssignValueInput';
14
+
15
+ const { mockUseFlowContext, mockGetDefaultBindingByField, mockVariableInput } = vi.hoisted(() => ({
16
+ mockUseFlowContext: vi.fn(),
17
+ mockGetDefaultBindingByField: vi.fn(),
18
+ mockVariableInput: vi.fn(() => <div data-testid="variable-input" />),
19
+ }));
20
+
21
+ vi.mock('@nocobase/flow-engine', async () => {
22
+ const actual = await vi.importActual<typeof import('@nocobase/flow-engine')>('@nocobase/flow-engine');
23
+ class MockEditableItemModel extends actual.EditableItemModel {}
24
+ MockEditableItemModel.getDefaultBindingByField = mockGetDefaultBindingByField;
25
+
26
+ return {
27
+ ...actual,
28
+ useFlowContext: () => mockUseFlowContext(),
29
+ VariableInput: mockVariableInput,
30
+ FlowModelRenderer: () => <div data-testid="flow-model-renderer" />,
31
+ EditableItemModel: MockEditableItemModel,
32
+ };
33
+ });
34
+
35
+ describe('FieldAssignValueInput context', () => {
36
+ it('delegates temporary field model context to the source form context', async () => {
37
+ type MockContext = {
38
+ dataSourceManager: { getDataSource: ReturnType<typeof vi.fn> };
39
+ t: (key: string) => string;
40
+ engine?: { createModel: ReturnType<typeof vi.fn> };
41
+ collection?: MockCollection;
42
+ blockModel?: MockFormModel;
43
+ };
44
+ type MockFieldModel = {
45
+ props: Record<string, unknown>;
46
+ setProps: ReturnType<typeof vi.fn>;
47
+ dispatchEvent: ReturnType<typeof vi.fn>;
48
+ remove: ReturnType<typeof vi.fn>;
49
+ };
50
+ type MockCollectionField = {
51
+ name: string;
52
+ interface: string;
53
+ uiSchema: { enum: Array<{ label: string; value: string }> };
54
+ isAssociationField: () => boolean;
55
+ getComponentProps: () => Record<string, unknown>;
56
+ };
57
+ type MockCollection = {
58
+ dataSourceKey: string;
59
+ name: string;
60
+ getField: (name: string) => MockCollectionField | null;
61
+ getFields: () => MockCollectionField[];
62
+ };
63
+ type MockFormModel = {
64
+ context: MockContext;
65
+ collection: MockCollection;
66
+ subModels: Record<string, unknown>;
67
+ };
68
+
69
+ const sourceContext: MockContext = {
70
+ dataSourceManager: {
71
+ getDataSource: vi.fn(() => ({})),
72
+ },
73
+ t: (key: string) => key,
74
+ };
75
+ const fieldModel: MockFieldModel = {
76
+ props: {},
77
+ setProps: vi.fn((props: Record<string, unknown>) => {
78
+ fieldModel.props = { ...fieldModel.props, ...props };
79
+ }),
80
+ dispatchEvent: vi.fn(),
81
+ remove: vi.fn(),
82
+ };
83
+ const tempRoot = {
84
+ context: {
85
+ defineProperty: vi.fn(),
86
+ },
87
+ subModels: {
88
+ fields: [fieldModel],
89
+ },
90
+ setProps: vi.fn(),
91
+ remove: vi.fn(),
92
+ };
93
+ const engine = {
94
+ createModel: vi.fn(() => tempRoot),
95
+ };
96
+ sourceContext.engine = engine;
97
+
98
+ const collectionField: MockCollectionField = {
99
+ name: 'status',
100
+ interface: 'select',
101
+ uiSchema: {
102
+ enum: [{ label: 'Open', value: 'open' }],
103
+ },
104
+ isAssociationField: () => false,
105
+ getComponentProps: () => ({}),
106
+ };
107
+ const collection: MockCollection = {
108
+ dataSourceKey: 'main',
109
+ name: 'tasks',
110
+ getField: (name: string) => (name === 'status' ? collectionField : null),
111
+ getFields: () => [collectionField],
112
+ };
113
+ const formModel: MockFormModel = {
114
+ context: sourceContext,
115
+ collection,
116
+ subModels: {},
117
+ };
118
+ sourceContext.collection = collection;
119
+ sourceContext.blockModel = formModel;
120
+
121
+ mockGetDefaultBindingByField.mockReturnValue({ modelName: 'SelectFieldModel' });
122
+ mockUseFlowContext.mockReturnValue({
123
+ model: formModel,
124
+ t: (key: string) => key,
125
+ getPropertyMetaTree: vi.fn(async () => []),
126
+ });
127
+
128
+ render(<FieldAssignValueInput targetPath="status" value="" onChange={vi.fn()} />);
129
+
130
+ await waitFor(() => {
131
+ expect(engine.createModel).toHaveBeenCalled();
132
+ });
133
+ expect(engine.createModel).toHaveBeenCalledWith(expect.any(Object), { delegate: sourceContext });
134
+ });
135
+
136
+ it('lets callers override variable path parsing for domain-specific stored formats', async () => {
137
+ mockVariableInput.mockClear();
138
+ const sourceContext = {
139
+ dataSourceManager: {
140
+ getDataSource: vi.fn(() => ({})),
141
+ },
142
+ t: (key: string) => key,
143
+ };
144
+ const fieldModel = {
145
+ props: {},
146
+ setProps: vi.fn(),
147
+ dispatchEvent: vi.fn(),
148
+ remove: vi.fn(),
149
+ };
150
+ const tempRoot = {
151
+ context: {
152
+ defineProperty: vi.fn(),
153
+ },
154
+ subModels: {
155
+ fields: [fieldModel],
156
+ },
157
+ setProps: vi.fn(),
158
+ remove: vi.fn(),
159
+ };
160
+ const engine = {
161
+ createModel: vi.fn(() => tempRoot),
162
+ };
163
+ const collectionField = {
164
+ name: 'status',
165
+ interface: 'input',
166
+ uiSchema: { 'x-component': 'Input' },
167
+ isAssociationField: () => false,
168
+ getComponentProps: () => ({}),
169
+ };
170
+ const collection = {
171
+ dataSourceKey: 'main',
172
+ name: 'tasks',
173
+ getField: (name: string) => (name === 'status' ? collectionField : null),
174
+ getFields: () => [collectionField],
175
+ };
176
+ const formModel = {
177
+ context: { ...sourceContext, engine, collection, blockModel: null as any },
178
+ collection,
179
+ subModels: {},
180
+ };
181
+ formModel.context.blockModel = formModel;
182
+
183
+ mockGetDefaultBindingByField.mockReturnValue({ modelName: 'InputFieldModel' });
184
+ mockUseFlowContext.mockReturnValue({
185
+ model: formModel,
186
+ t: (key: string) => key,
187
+ getPropertyMetaTree: vi.fn(async () => []),
188
+ });
189
+
190
+ const variableConverters = {
191
+ resolvePathFromValue: vi.fn((value: string) =>
192
+ value === '{{$context.data.updatedAt}}' ? ['$context', 'data', 'updatedAt'] : undefined,
193
+ ),
194
+ resolveValueFromPath: vi.fn(() => undefined),
195
+ };
196
+
197
+ render(
198
+ <FieldAssignValueInput
199
+ targetPath="status"
200
+ value="{{$context.data.updatedAt}}"
201
+ onChange={vi.fn()}
202
+ variableConverters={variableConverters}
203
+ />,
204
+ );
205
+
206
+ await waitFor(() => {
207
+ expect(engine.createModel).toHaveBeenCalled();
208
+ });
209
+ const latestVariableInputCall = mockVariableInput.mock.calls.at(-1)?.[0];
210
+ expect(latestVariableInputCall.converters.resolvePathFromValue('{{$context.data.updatedAt}}')).toEqual([
211
+ '$context',
212
+ 'data',
213
+ 'updatedAt',
214
+ ]);
215
+ });
216
+ });
@@ -45,6 +45,7 @@ interface FilterGroupProps {
45
45
  value: Record<string, any>;
46
46
  /** 自定义筛选项组件 */
47
47
  FilterItem?: React.FC<FilterItemProps>;
48
+ disabled?: boolean;
48
49
  closeIcon?: ReactNode;
49
50
  /** 是否显示边框 */
50
51
  showBorder?: boolean;
@@ -89,6 +90,7 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
89
90
  const {
90
91
  value = { logic: '$and', items: [] },
91
92
  FilterItem,
93
+ disabled = false,
92
94
  showBorder = false,
93
95
  onRemove,
94
96
  onChange,
@@ -120,11 +122,17 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
120
122
  };
121
123
 
122
124
  const handleLogicChange = (newLogic: '$and' | '$or') => {
125
+ if (disabled) {
126
+ return;
127
+ }
123
128
  value.logic = newLogic;
124
129
  onChange?.(value);
125
130
  };
126
131
 
127
132
  const handleAddCondition = () => {
133
+ if (disabled) {
134
+ return;
135
+ }
128
136
  items.push({
129
137
  path: '',
130
138
  operator: '',
@@ -134,6 +142,9 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
134
142
  };
135
143
 
136
144
  const handleAddConditionGroup = () => {
145
+ if (disabled) {
146
+ return;
147
+ }
137
148
  items.push({
138
149
  logic: '$and',
139
150
  items: [],
@@ -142,6 +153,9 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
142
153
  };
143
154
 
144
155
  const handleRemoveItem = (index: number) => {
156
+ if (disabled) {
157
+ return;
158
+ }
145
159
  items.splice(index, 1);
146
160
  onChange?.(value);
147
161
  };
@@ -160,11 +174,12 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
160
174
  <button
161
175
  type="button"
162
176
  aria-label="icon-close"
177
+ disabled={disabled}
163
178
  style={{
164
179
  position: 'absolute',
165
180
  right: 10,
166
181
  top: 10,
167
- cursor: 'pointer',
182
+ cursor: disabled ? 'not-allowed' : 'pointer',
168
183
  background: 'transparent',
169
184
  border: 0,
170
185
  padding: 0,
@@ -182,6 +197,7 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
182
197
  data-testid="filter-select-all-or-any"
183
198
  style={{ width: 'auto' }}
184
199
  value={logic}
200
+ disabled={disabled}
185
201
  onChange={handleLogicChange}
186
202
  >
187
203
  <Select.Option value="$and">{t('All')}</Select.Option>
@@ -200,6 +216,7 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
200
216
  key={getFilterItemKey(item)}
201
217
  value={item}
202
218
  FilterItem={FilterItem}
219
+ disabled={disabled}
203
220
  showBorder={true}
204
221
  onRemove={() => handleRemoveItem(index)}
205
222
  onChange={(v) => {
@@ -221,11 +238,12 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
221
238
  <button
222
239
  type="button"
223
240
  aria-label="icon-close"
241
+ disabled={disabled}
224
242
  style={{
225
243
  marginLeft: 8,
226
244
  marginRight: 8,
227
245
  flex: '0 0 auto',
228
- cursor: 'pointer',
246
+ cursor: disabled ? 'not-allowed' : 'pointer',
229
247
  background: 'transparent',
230
248
  border: 0,
231
249
  padding: 0,
@@ -251,11 +269,12 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
251
269
  <button
252
270
  type="button"
253
271
  aria-label="icon-close"
272
+ disabled={disabled}
254
273
  style={{
255
274
  marginLeft: 8,
256
275
  marginRight: 8,
257
276
  flex: '0 0 auto',
258
- cursor: 'pointer',
277
+ cursor: disabled ? 'not-allowed' : 'pointer',
259
278
  background: 'transparent',
260
279
  border: 0,
261
280
  padding: 0,
@@ -277,11 +296,12 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
277
296
  <button
278
297
  type="button"
279
298
  aria-label="icon-close"
299
+ disabled={disabled}
280
300
  style={{
281
301
  marginLeft: 8,
282
302
  marginRight: 8,
283
303
  flex: '0 0 auto',
284
- cursor: 'pointer',
304
+ cursor: disabled ? 'not-allowed' : 'pointer',
285
305
  background: 'transparent',
286
306
  border: 0,
287
307
  padding: 0,
@@ -298,7 +318,14 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
298
318
  </div>
299
319
 
300
320
  <Space size={16} style={{ marginTop: 8, marginBottom: 8 }}>
301
- <Button style={{ padding: 0 }} type="link" size="small" icon={<PlusOutlined />} onClick={handleAddCondition}>
321
+ <Button
322
+ style={{ padding: 0 }}
323
+ type="link"
324
+ size="small"
325
+ icon={<PlusOutlined />}
326
+ onClick={handleAddCondition}
327
+ disabled={disabled}
328
+ >
302
329
  {t('Add condition')}
303
330
  </Button>
304
331
  <Button
@@ -307,6 +334,7 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
307
334
  size="small"
308
335
  icon={<PlusOutlined />}
309
336
  onClick={handleAddConditionGroup}
337
+ disabled={disabled}
310
338
  >
311
339
  {t('Add condition group')}
312
340
  </Button>