@nocobase/client-v2 2.2.0-beta.15 → 2.2.0-beta.17

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 (93) hide show
  1. package/es/components/form/ScanInput/useCodeScanner.d.ts +2 -2
  2. package/es/flow/actions/linkageRules.d.ts +25 -0
  3. package/es/flow/components/FieldAssignValueInput.d.ts +4 -30
  4. package/es/flow/components/FlowRoute.d.ts +2 -2
  5. package/es/flow/components/field-value-variable/DateVariableEditor.d.ts +24 -0
  6. package/es/flow/components/field-value-variable/FieldValueVariableInput.d.ts +31 -0
  7. package/es/flow/components/field-value-variable/dateValue.d.ts +36 -0
  8. package/es/flow/components/field-value-variable/index.d.ts +11 -0
  9. package/es/flow/internal/registerDeviceTypeContext.d.ts +10 -0
  10. package/es/flow/models/actions/UpdateRecordActionUtils.d.ts +8 -2
  11. package/es/flow/models/base/ActionModelCore.d.ts +4 -2
  12. package/es/flow/models/base/BlockGridModel.d.ts +3 -0
  13. package/es/flow/models/blocks/form/FormBlockModel.d.ts +6 -0
  14. package/es/flow/models/blocks/form/FormGridModel.d.ts +6 -0
  15. package/es/flow/models/blocks/form/value-runtime/rules.d.ts +1 -0
  16. package/es/flow/models/blocks/table/JSColumnModel.d.ts +2 -0
  17. package/es/flow/models/blocks/table/TableColumnModel.d.ts +1 -0
  18. package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +15 -7
  19. package/es/index.d.ts +1 -0
  20. package/es/index.mjs +105 -111
  21. package/es/ui-operation/index.d.ts +15 -0
  22. package/es/ui-operation/ui-operation-codec.d.ts +10 -0
  23. package/lib/index.js +108 -114
  24. package/lib/locale/languageCodes.js +1 -0
  25. package/package.json +8 -7
  26. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +7 -2
  27. package/src/components/form/ScanInput/CodeScanner.tsx +61 -34
  28. package/src/components/form/ScanInput/__tests__/CodeScanner.test.tsx +101 -0
  29. package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +114 -18
  30. package/src/components/form/ScanInput/useCodeScanner.ts +166 -23
  31. package/src/flow/__tests__/FlowRoute.test.tsx +126 -8
  32. package/src/flow/__tests__/PluginFlowEngine.test.ts +58 -0
  33. package/src/flow/actions/__tests__/actionLinkageRules.forkProps.test.ts +314 -0
  34. package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +102 -0
  35. package/src/flow/actions/__tests__/linkageRules.actionStates.test.ts +33 -0
  36. package/src/flow/actions/linkageRules.tsx +51 -13
  37. package/src/flow/common/Markdown/style.ts +4 -0
  38. package/src/flow/components/BlockItemCard.tsx +2 -2
  39. package/src/flow/components/FieldAssignValueInput.tsx +38 -621
  40. package/src/flow/components/FlowRoute.tsx +71 -29
  41. package/src/flow/components/field-value-variable/DateVariableEditor.tsx +181 -0
  42. package/src/flow/components/field-value-variable/FieldValueVariableInput.tsx +326 -0
  43. package/src/flow/components/field-value-variable/__tests__/FieldValueVariableInput.test.tsx +380 -0
  44. package/src/flow/components/field-value-variable/dateValue.ts +223 -0
  45. package/src/flow/components/field-value-variable/index.ts +12 -0
  46. package/src/flow/index.ts +2 -0
  47. package/src/flow/internal/registerDeviceTypeContext.ts +39 -0
  48. package/src/flow/models/actions/UpdateRecordActionModel.tsx +18 -1
  49. package/src/flow/models/actions/UpdateRecordActionUtils.ts +18 -6
  50. package/src/flow/models/actions/__tests__/UpdateRecordActionModel.test.ts +76 -4
  51. package/src/flow/models/base/ActionModelCore.tsx +11 -2
  52. package/src/flow/models/base/BlockGridModel.tsx +26 -0
  53. package/src/flow/models/base/CollectionBlockModel.tsx +15 -2
  54. package/src/flow/models/base/PageModel/PageTabModel.tsx +67 -17
  55. package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +509 -12
  56. package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +49 -0
  57. package/src/flow/models/base/__tests__/BlockGridModel.selectSceneActivation.test.ts +124 -0
  58. package/src/flow/models/base/__tests__/CollectionBlockModel.addAppends.test.ts +41 -0
  59. package/src/flow/models/base/__tests__/CollectionBlockModel.initialBeforeRenderRefresh.test.ts +60 -0
  60. package/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx +28 -53
  61. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +27 -3
  62. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +32 -4
  63. package/src/flow/models/blocks/form/FormBlockModel.tsx +4 -0
  64. package/src/flow/models/blocks/form/FormGridModel.tsx +4 -0
  65. package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +8 -2
  66. package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +435 -0
  67. package/src/flow/models/blocks/form/value-runtime/rules.ts +67 -14
  68. package/src/flow/models/blocks/form/value-runtime/runtime.ts +43 -19
  69. package/src/flow/models/blocks/js-block/JSBlock.tsx +1 -1
  70. package/src/flow/models/blocks/table/JSColumnModel.tsx +10 -1
  71. package/src/flow/models/blocks/table/TableBlockModel.tsx +3 -1
  72. package/src/flow/models/blocks/table/TableColumnModel.tsx +30 -1
  73. package/src/flow/models/blocks/table/__tests__/JSColumnModel.test.tsx +52 -5
  74. package/src/flow/models/blocks/table/__tests__/TableBlockModel.quickEditRefresh.test.ts +12 -0
  75. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +33 -0
  76. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +8 -0
  77. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/__tests__/popupContext.test.ts +120 -0
  78. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/actions/PopupSubTableEditActionModel.tsx +10 -2
  79. package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +46 -0
  80. package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +149 -0
  81. package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +149 -113
  82. package/src/flow/models/fields/JsonFieldModel.tsx +31 -8
  83. package/src/flow/models/fields/__tests__/JsonFieldModel.test.ts +82 -0
  84. package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +1 -0
  85. package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +20 -2
  86. package/src/flow/utils/dataScopeFormValueClear.ts +4 -3
  87. package/src/index.ts +1 -0
  88. package/src/layout-manager/LayoutContentRoute.tsx +2 -1
  89. package/src/layout-manager/LayoutRoute.tsx +2 -1
  90. package/src/layout-manager/__tests__/LayoutRoute.test.tsx +87 -1
  91. package/src/locale/languageCodes.ts +1 -0
  92. package/src/ui-operation/index.ts +33 -0
  93. package/src/ui-operation/ui-operation-codec.ts +54 -0
@@ -0,0 +1,124 @@
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 { FlowEngine, type FlowModelContext, MultiRecordResource, VIEW_ACTIVATED_EVENT } from '@nocobase/flow-engine';
11
+ import { beforeEach, describe, expect, it } from 'vitest';
12
+ import { BlockGridModel } from '../BlockGridModel';
13
+ import { CollectionBlockModel } from '../CollectionBlockModel';
14
+
15
+ class TestBlockGridModel extends BlockGridModel {
16
+ mountForTest() {
17
+ super.onMount();
18
+ }
19
+
20
+ unmountForTest() {
21
+ super.onUnmount();
22
+ }
23
+ }
24
+
25
+ class TestMultiRecordResource extends MultiRecordResource<Record<string, unknown>> {
26
+ refreshCalls = 0;
27
+
28
+ override async refresh(): Promise<void> {
29
+ this.refreshCalls += 1;
30
+ this.emit('refresh');
31
+ }
32
+ }
33
+
34
+ class TestCollectionBlockModel extends CollectionBlockModel {
35
+ createResource(ctx: FlowModelContext) {
36
+ return ctx.createResource(TestMultiRecordResource);
37
+ }
38
+
39
+ mountForTest() {
40
+ super.onMount();
41
+ }
42
+ }
43
+
44
+ describe('BlockGridModel - select scene activation', () => {
45
+ let engine: FlowEngine;
46
+
47
+ beforeEach(() => {
48
+ engine = new FlowEngine();
49
+ engine.context.defineProperty('location', { value: { search: '' } });
50
+ engine.registerModels({ TestBlockGridModel, TestCollectionBlockModel });
51
+
52
+ engine.dataSourceManager.getDataSource('main').addCollection({
53
+ name: 'roles',
54
+ filterTargetKey: 'id',
55
+ fields: [
56
+ { name: 'id', type: 'integer', interface: 'number' },
57
+ { name: 'name', type: 'string', interface: 'input' },
58
+ ],
59
+ });
60
+ });
61
+
62
+ function createModels(scene = 'select') {
63
+ engine.context.defineProperty('view', { value: { inputArgs: { scene } } });
64
+ const grid = engine.createModel<TestBlockGridModel>({ use: 'TestBlockGridModel' });
65
+ const block = grid.addSubModel<TestCollectionBlockModel>('items', {
66
+ use: 'TestCollectionBlockModel',
67
+ stepParams: {
68
+ resourceSettings: {
69
+ init: {
70
+ dataSourceKey: 'main',
71
+ collectionName: 'roles',
72
+ },
73
+ },
74
+ },
75
+ });
76
+ const resource = block.context.resource as TestMultiRecordResource;
77
+
78
+ block.mountForTest();
79
+ grid.mountForTest();
80
+ return { grid, resource };
81
+ }
82
+
83
+ it('refreshes dirty collection blocks once when a nested view closes', async () => {
84
+ const { grid, resource } = createModels();
85
+
86
+ engine.markDataSourceDirty('main', 'roles');
87
+ engine.emitter.emit(VIEW_ACTIVATED_EVENT);
88
+ await Promise.resolve();
89
+
90
+ expect(resource.refreshCalls).toBe(1);
91
+
92
+ engine.emitter.emit(VIEW_ACTIVATED_EVENT);
93
+ await Promise.resolve();
94
+
95
+ expect(resource.refreshCalls).toBe(1);
96
+ grid.unmountForTest();
97
+ });
98
+
99
+ it('does not refresh without dirty data or after the grid unmounts', async () => {
100
+ const { grid, resource } = createModels();
101
+
102
+ engine.emitter.emit(VIEW_ACTIVATED_EVENT);
103
+ await Promise.resolve();
104
+ expect(resource.refreshCalls).toBe(0);
105
+
106
+ grid.unmountForTest();
107
+ engine.markDataSourceDirty('main', 'roles');
108
+ engine.emitter.emit(VIEW_ACTIVATED_EVENT);
109
+ await Promise.resolve();
110
+
111
+ expect(resource.refreshCalls).toBe(0);
112
+ });
113
+
114
+ it('does not handle activation outside the select scene', async () => {
115
+ const { grid, resource } = createModels('list');
116
+
117
+ engine.markDataSourceDirty('main', 'roles');
118
+ engine.emitter.emit(VIEW_ACTIVATED_EVENT);
119
+ await Promise.resolve();
120
+
121
+ expect(resource.refreshCalls).toBe(0);
122
+ grid.unmountForTest();
123
+ });
124
+ });
@@ -0,0 +1,41 @@
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 { FlowEngine, FlowModelContext, MultiRecordResource } from '@nocobase/flow-engine';
11
+ import { describe, expect, it } from 'vitest';
12
+ import { CollectionBlockModel } from '../CollectionBlockModel';
13
+
14
+ class AddAppendsCollectionBlockModel extends CollectionBlockModel {
15
+ createResource(ctx: FlowModelContext) {
16
+ return ctx.createResource(MultiRecordResource);
17
+ }
18
+ }
19
+
20
+ describe('CollectionBlockModel addAppends', () => {
21
+ it('does not throw for a nested field path when the collection is unavailable', () => {
22
+ const engine = new FlowEngine();
23
+ engine.registerModels({ AddAppendsCollectionBlockModel });
24
+
25
+ const model = engine.createModel<AddAppendsCollectionBlockModel>({
26
+ uid: 'missing-collection-add-appends-block',
27
+ use: 'AddAppendsCollectionBlockModel',
28
+ stepParams: {
29
+ resourceSettings: {
30
+ init: {
31
+ dataSourceKey: 'main',
32
+ collectionName: 'missing_collection',
33
+ },
34
+ },
35
+ },
36
+ });
37
+
38
+ expect(model.collection).toBeUndefined();
39
+ expect(() => model.addAppends('createdBy.departments')).not.toThrow();
40
+ });
41
+ });
@@ -128,4 +128,64 @@ describe('CollectionBlockModel initial beforeRender refresh', () => {
128
128
  expect(resource.getData()).toEqual([]);
129
129
  expect(resource.getMeta('count')).toBe(0);
130
130
  });
131
+
132
+ it('skips forced active refresh in manual mode when filters are empty', async () => {
133
+ const { model, resource } = setupModelWithManualMode();
134
+ const refreshSpy = vi.spyOn(resource, 'refresh');
135
+
136
+ resource.setData([{ id: 2, name: 'Stale' }]);
137
+ resource.setMeta({ count: 1, hasNext: true });
138
+ resource.setPage(2);
139
+ resource.loading = true;
140
+
141
+ model.onActive(true);
142
+ await Promise.resolve();
143
+ await Promise.resolve();
144
+
145
+ expect(refreshSpy).not.toHaveBeenCalled();
146
+ expect(resource.getData()).toEqual([]);
147
+ expect(resource.getMeta('count')).toBe(0);
148
+ expect(resource.getMeta('hasNext')).toBe(false);
149
+ expect(resource.getMeta('page')).toBe(1);
150
+ expect(resource.getRequestParameter('page')).toBe(1);
151
+ expect(resource.loading).toBe(false);
152
+ });
153
+
154
+ it('keeps forced active refresh in manual mode when filters are active', async () => {
155
+ const { model, resource } = setupModelWithManualMode();
156
+ const refreshSpy = vi.spyOn(resource, 'refresh').mockResolvedValue();
157
+
158
+ resource.setData([{ id: 2, name: 'Stale' }]);
159
+ resource.setMeta({ count: 1, hasNext: true, page: 2 });
160
+ model.setFilterActive('filter-form-item', true);
161
+
162
+ model.onActive(true);
163
+ await Promise.resolve();
164
+
165
+ expect(refreshSpy).toHaveBeenCalledTimes(1);
166
+ expect(resource.getData()).toEqual([{ id: 2, name: 'Stale' }]);
167
+ expect(resource.getMeta('count')).toBe(1);
168
+ });
169
+
170
+ it('skips forced active refresh in manual mode when only resource filters are active', async () => {
171
+ const { model, resource } = setupModelWithManualMode();
172
+ const refreshSpy = vi.spyOn(resource, 'refresh');
173
+
174
+ resource.setData([{ id: 2, name: 'Stale' }]);
175
+ resource.setMeta({ count: 1, hasNext: true });
176
+ resource.setPage(2);
177
+ resource.loading = true;
178
+ resource.addFilterGroup('data-scope', { status: { $eq: 'active' } });
179
+
180
+ model.onActive(true);
181
+ await Promise.resolve();
182
+
183
+ expect(refreshSpy).not.toHaveBeenCalled();
184
+ expect(resource.getData()).toEqual([]);
185
+ expect(resource.getMeta('count')).toBe(0);
186
+ expect(resource.getMeta('hasNext')).toBe(false);
187
+ expect(resource.getMeta('page')).toBe(1);
188
+ expect(resource.getRequestParameter('page')).toBe(1);
189
+ expect(resource.loading).toBe(false);
190
+ });
131
191
  });
@@ -10,23 +10,20 @@
10
10
  import React from 'react';
11
11
  import { Input } from 'antd';
12
12
  import { define, observable } from '@formily/reactive';
13
- import {
14
- FlowModelRenderer,
15
- FormItem,
16
- VariableInput,
17
- tExpr,
18
- isVariableExpression,
19
- parseValueToPath,
20
- isRunJSValue,
21
- EditableItemModel,
22
- jioToJoiSchema,
23
- } from '@nocobase/flow-engine';
13
+ import { FlowModelRenderer, FormItem, tExpr, EditableItemModel, jioToJoiSchema } from '@nocobase/flow-engine';
24
14
  // 无需类型导入(避免未使用的类型)
25
15
  import { FormItemModel } from '../form/FormItemModel';
26
16
  import { EditFormModel } from '../form/EditFormModel';
27
17
  import { customAlphabet as Alphabet } from 'nanoid';
28
18
  import { ensureOptionsFromUiSchemaEnumIfAbsent } from '../../../internal/utils/enumOptionsUtils';
29
19
  import { RunJSValueEditor } from '../../../components/RunJSValueEditor';
20
+ import {
21
+ DEFAULT_DATE_VARIABLE_COMPONENT_PROPS,
22
+ FieldValueVariableInput,
23
+ getFieldInterface,
24
+ isDateLikeField,
25
+ resolveDateVariableComponentProps,
26
+ } from '../../../components/field-value-variable';
30
27
 
31
28
  type AssignFormTempOriginField = {
32
29
  uid?: string;
@@ -285,7 +282,9 @@ export class AssignFormItemModel extends FormItemModel {
285
282
  ) : null;
286
283
  };
287
284
 
288
- const NullComponent: React.FC = () => <Input placeholder={'<Null>'} readOnly style={{ width: '100%' }} />;
285
+ const NullComponent: React.FC = () => (
286
+ <Input placeholder={`<${this.context.t?.('Null') ?? 'Null'}>`} readOnly style={{ width: '100%' }} />
287
+ );
289
288
 
290
289
  const RunJSComponent: React.FC<any> = (inputProps: any) => {
291
290
  return (
@@ -298,46 +297,18 @@ export class AssignFormItemModel extends FormItemModel {
298
297
  );
299
298
  };
300
299
 
301
- const converters = {
302
- renderInputComponent: (meta: any) => {
303
- const firstPath = meta?.paths?.[0];
304
- if (firstPath === 'constant') return ConstantValueEditor as any;
305
- if (firstPath === 'null') return NullComponent as any;
306
- if (firstPath === 'runjs') return RunJSComponent as any;
307
- return undefined;
308
- },
309
- resolveValueFromPath: (item: any) => {
310
- const firstPath = item?.paths?.[0];
311
- if (firstPath === 'constant') return '';
312
- if (firstPath === 'null') return null;
313
- if (firstPath === 'runjs') return { code: '', version: 'v2' };
314
- return undefined;
315
- },
316
- resolvePathFromValue: (currentValue: any) => {
317
- if (currentValue === null) return ['null'];
318
- if (isRunJSValue(currentValue)) return ['runjs'];
319
- return isVariableExpression(currentValue) ? parseValueToPath(currentValue) : ['constant'];
320
- },
321
- } as any;
322
-
323
- // 合并变量树:在最前面追加“常量/空值”两个选项
324
- const mergedMetaTree = async () => {
300
+ const baseMetaTree = async () => {
325
301
  const getTree = (this.context as any)?.getPropertyMetaTree;
326
- const base: any[] = typeof getTree === 'function' ? await getTree() : [];
327
- return [
328
- {
329
- title: tExpr('Constant'),
330
- name: 'constant',
331
- type: 'string',
332
- paths: ['constant'],
333
- render: ConstantValueEditor,
334
- },
335
- { title: tExpr('Null'), name: 'null', type: 'object', paths: ['null'], render: NullComponent },
336
- { title: tExpr('RunJS'), name: 'runjs', type: 'object', paths: ['runjs'], render: RunJSComponent },
337
- ...base,
338
- ];
302
+ return typeof getTree === 'function' ? await getTree() : [];
339
303
  };
340
304
 
305
+ const targetCollectionField = collection?.getField?.(this.fieldPath);
306
+ const targetFieldInterface = getFieldInterface(targetCollectionField);
307
+ const dateLike = isDateLikeField(targetCollectionField, this.fieldPath?.split('.').slice(-1)[0]);
308
+ const dateComponentProps = dateLike
309
+ ? resolveDateVariableComponentProps(targetCollectionField, targetFieldInterface)
310
+ : DEFAULT_DATE_VARIABLE_COMPONENT_PROPS;
311
+
341
312
  // 计算 label:优先使用配置中的 label,其次集合字段标题,最后回退字段路径
342
313
  let labelText = this.props?.label ?? this.fieldPath ?? '';
343
314
  const cf = collection?.getField?.(this.fieldPath);
@@ -357,14 +328,18 @@ export class AssignFormItemModel extends FormItemModel {
357
328
  const formValue = formBindingProps?.__assign_value__;
358
329
  const mergedValue = typeof formValue === 'undefined' ? this.assignValue : formValue;
359
330
  return (
360
- <VariableInput
331
+ <FieldValueVariableInput
361
332
  value={mergedValue}
362
- onChange={(v: any) => {
333
+ onChange={(v) => {
363
334
  this.assignValue = v;
364
335
  formBindingProps?.__assign_trigger__?.(v);
365
336
  }}
366
- metaTree={mergedMetaTree}
367
- converters={converters}
337
+ baseMetaTree={baseMetaTree}
338
+ constantComponent={ConstantValueEditor}
339
+ nullComponent={NullComponent}
340
+ runJSComponent={RunJSComponent}
341
+ isDateLikeField={dateLike}
342
+ dateComponentProps={dateComponentProps}
368
343
  clearValue={''}
369
344
  />
370
345
  );
@@ -15,8 +15,10 @@ import {
15
15
  DndProvider,
16
16
  DragHandler,
17
17
  Droppable,
18
+ isCtxDateExpression,
18
19
  isRunJSValue,
19
20
  normalizeRunJSValue,
21
+ parseCtxDateExpression,
20
22
  tExpr,
21
23
  FlowModelRenderer,
22
24
  FlowSettingsButton,
@@ -44,6 +46,15 @@ import { normalizeFilterValueByOperator } from './valueNormalization';
44
46
 
45
47
  const RELATION_FIELD_TYPES = ['belongsTo', 'hasOne', 'hasMany', 'belongsToMany', 'belongsToArray'];
46
48
  const NUMERIC_FIELD_TYPES = ['integer', 'float', 'double', 'decimal'];
49
+ const DATE_FILTER_OPERATORS = new Set([
50
+ '$dateOn',
51
+ '$dateNotOn',
52
+ '$dateBefore',
53
+ '$dateAfter',
54
+ '$dateNotBefore',
55
+ '$dateNotAfter',
56
+ '$dateBetween',
57
+ ]);
47
58
 
48
59
  function getFilterFormFieldMetaType(field: CollectionField) {
49
60
  if (RELATION_FIELD_TYPES.includes(field.type)) {
@@ -66,6 +77,14 @@ function getFilterFormFieldMetaType(field: CollectionField) {
66
77
  }
67
78
  }
68
79
 
80
+ function parseDateFilterDefaultValue(operator: string | undefined, rawValue: unknown) {
81
+ if (!operator || !DATE_FILTER_OPERATORS.has(operator) || !isCtxDateExpression(rawValue)) {
82
+ return undefined;
83
+ }
84
+
85
+ return parseCtxDateExpression(rawValue);
86
+ }
87
+
69
88
  function shouldShowFilterFormFieldMeta(field: CollectionField) {
70
89
  return Boolean(field?.interface);
71
90
  }
@@ -489,7 +508,7 @@ export class FilterFormBlockModel extends FilterBlockModel<{
489
508
  const rules = (params?.value || []) as any[];
490
509
  if (!Array.isArray(rules) || rules.length === 0) return appliedValues;
491
510
 
492
- const resolveValue = async (raw: any) => {
511
+ const resolveValue = async (raw: any, operator?: string) => {
493
512
  // RunJS support
494
513
  if (isRunJSValue(raw)) {
495
514
  const { code, version } = normalizeRunJSValue(raw);
@@ -497,6 +516,11 @@ export class FilterFormBlockModel extends FilterBlockModel<{
497
516
  return ret?.success ? ret.value : undefined;
498
517
  }
499
518
 
519
+ const parsedDateFilterValue = parseDateFilterDefaultValue(operator, raw);
520
+ if (typeof parsedDateFilterValue !== 'undefined') {
521
+ return parsedDateFilterValue;
522
+ }
523
+
500
524
  return await (this.context as any).resolveJsonTemplate?.(raw);
501
525
  };
502
526
 
@@ -520,11 +544,11 @@ export class FilterFormBlockModel extends FilterBlockModel<{
520
544
 
521
545
  const current = (form as any).getFieldValue?.(name);
522
546
 
523
- const resolved = await resolveValue(rule.value);
547
+ const operator = getDefaultOperator(itemModel as any);
548
+ const resolved = await resolveValue(rule.value, operator);
524
549
  if (options?.refreshSeq && options.refreshSeq !== this.defaultValuesRefreshSeq) return appliedValues;
525
550
  if (typeof resolved === 'undefined') continue;
526
551
 
527
- const operator = getDefaultOperator(itemModel as any);
528
552
  const normalized = normalizeFilterValueByOperator(operator, resolved);
529
553
  const mode = this.normalizeFieldValueMode(rule.mode);
530
554
  if (mode === 'default' && !this.canApplyFormDefaultValue(String(name), current, force)) continue;
@@ -17,7 +17,15 @@ import { FilterFormBlockModel } from '../FilterFormBlockModel';
17
17
  function resolveTemplateValue(raw: any, values: Record<string, any>): any {
18
18
  if (typeof raw === 'string') {
19
19
  const matched = raw.match(/^\{\{\s*ctx\.formValues\.([^}]+?)\s*\}\}$/);
20
- return matched ? values[matched[1]] : raw;
20
+ if (matched) {
21
+ return values[matched[1]];
22
+ }
23
+
24
+ if (/^\{\{\s*ctx\.date\.relative\.past\.day\.n7\s*\}\}$/.test(raw)) {
25
+ return '2026-06-15';
26
+ }
27
+
28
+ return raw;
21
29
  }
22
30
  if (Array.isArray(raw)) {
23
31
  return raw.map((item) => resolveTemplateValue(item, values));
@@ -30,7 +38,7 @@ function resolveTemplateValue(raw: any, values: Record<string, any>): any {
30
38
 
31
39
  function createFilterFormDefaultValuesModel(rules: any[], initialValues: Record<string, any> = {}) {
32
40
  const values = { ...initialValues };
33
- const createItem = (fieldPath: string, uid: string) => ({
41
+ const createItem = (fieldPath: string, uid: string, operator?: string) => ({
34
42
  uid,
35
43
  fieldPath,
36
44
  props: { name: `${fieldPath}_${uid}` },
@@ -44,7 +52,7 @@ function createFilterFormDefaultValuesModel(rules: any[], initialValues: Record<
44
52
  return undefined;
45
53
  },
46
54
  subModels: {
47
- field: {},
55
+ field: operator ? { operator } : {},
48
56
  },
49
57
  });
50
58
  const model = {
@@ -76,7 +84,11 @@ function createFilterFormDefaultValuesModel(rules: any[], initialValues: Record<
76
84
  subModels: {
77
85
  grid: {
78
86
  subModels: {
79
- items: [createItem('nickname', 'nick'), createItem('username', 'user')],
87
+ items: [
88
+ createItem('nickname', 'nick'),
89
+ createItem('username', 'user'),
90
+ createItem('birthdate_tz', 'birthdate', '$dateOn'),
91
+ ],
80
92
  },
81
93
  },
82
94
  },
@@ -354,6 +366,22 @@ describe('filter-form defaultValues wiring', () => {
354
366
  expect(values.username_user).toBe('Bob');
355
367
  });
356
368
 
369
+ it('preserves relative date descriptors for date filter default values', async () => {
370
+ const { model, values } = createFilterFormDefaultValuesModel([
371
+ {
372
+ key: 'birthdate-default',
373
+ enable: true,
374
+ targetPath: 'birthdate_tz',
375
+ mode: 'assign',
376
+ value: '{{ ctx.date.relative.past.day.n7 }}',
377
+ },
378
+ ]);
379
+
380
+ await FilterFormBlockModel.prototype.applyFormDefaultValues.call(model as any);
381
+
382
+ expect(values.birthdate_tz_birthdate).toEqual({ type: 'past', unit: 'day', number: 7 });
383
+ });
384
+
357
385
  it('applies override values until the target filter field is changed by user', async () => {
358
386
  const { model, values } = createFilterFormDefaultValuesModel(
359
387
  [
@@ -77,6 +77,10 @@ export class FormBlockModel<
77
77
  > extends CollectionBlockModel<T> {
78
78
  formValueRuntime?: FormValueRuntime;
79
79
 
80
+ serialize() {
81
+ return { ...super.serialize(), variableContractType: { type: 'form', use: this.use } };
82
+ }
83
+
80
84
  private userModifiedTopLevelFields = new Set<string>();
81
85
 
82
86
  get form() {
@@ -21,6 +21,10 @@ export type DefaultFormGridStructure = {
21
21
  };
22
22
 
23
23
  export class FormGridModel<T extends DefaultFormGridStructure = DefaultFormGridStructure> extends GridModel<T> {
24
+ serialize() {
25
+ return { ...super.serialize(), variableContractType: { type: 'formGrid', use: this.use } };
26
+ }
27
+
24
28
  itemFallback = (<Skeleton.Input block size="small" style={{ marginBottom: '0.5rem' }} />);
25
29
  itemSettingsMenuLevel = 2;
26
30
  itemFlowSettings = {
@@ -465,11 +465,17 @@ describe('FormBlockModel (form/formValues injection & server resolve anchors)',
465
465
 
466
466
  it('builds non-empty contextParams for ctx.formValues.* deep association path', async () => {
467
467
  const model = await setupFormModel();
468
+ const sessionPayload = Buffer.from(JSON.stringify({ userId: 1, signInTime: 'form-record-slots' })).toString(
469
+ 'base64url',
470
+ );
468
471
  // 注入 api mock 到引擎上下文,拦截 variables:resolve 的请求
469
472
  const api = {
473
+ auth: { token: `test.${sessionPayload}.sig` },
470
474
  request: vi.fn(async (config: any) => {
471
- const payload = config?.data?.values || {};
472
- const batch = payload.batch || [];
475
+ const requestValues = config?.data?.values || {};
476
+ const batch = requestValues.batch || [];
477
+ expect(batch[0]?.rd).toEqual(expect.any(String));
478
+ expect(batch[0]?.template).toEqual({ who: '{{ ctx.formValues.assignees.org.name }}' });
473
479
  const cp = batch[0]?.contextParams || {};
474
480
  const keys = Object.keys(cp).sort();
475
481
  // 聚合为单键,不再使用索引键