@nocobase/client-v2 2.2.0-beta.9 → 2.2.0

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 (224) hide show
  1. package/es/APIClient.d.ts +13 -3
  2. package/es/BaseApplication.d.ts +1 -0
  3. package/es/collection-field-interface/CollectionFieldInterface.d.ts +1 -0
  4. package/es/collection-field-interface/CollectionFieldInterfaceManager.d.ts +1 -0
  5. package/es/components/category-tabs/SortableCategoryTabs.d.ts +55 -0
  6. package/es/components/category-tabs/index.d.ts +9 -0
  7. package/es/components/form/ScanInput/useCodeScanner.d.ts +3 -2
  8. package/es/components/form/TypedVariableInput.d.ts +31 -5
  9. package/es/components/form/filter/CollectionFilter.d.ts +2 -0
  10. package/es/components/form/filter/CollectionFilterItem.d.ts +11 -1
  11. package/es/components/form/filter/CollectionFilterPanel.d.ts +2 -0
  12. package/es/components/form/filter/index.d.ts +2 -0
  13. package/es/components/form/filter/useFilterActionProps.d.ts +2 -0
  14. package/es/components/index.d.ts +1 -0
  15. package/es/flow/actions/index.d.ts +1 -1
  16. package/es/flow/actions/linkageRules.d.ts +27 -0
  17. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +1 -0
  18. package/es/flow/components/FieldAssignExactDatePicker.d.ts +1 -0
  19. package/es/flow/components/FieldAssignValueInput.d.ts +7 -24
  20. package/es/flow/components/FlowRoute.d.ts +2 -2
  21. package/es/flow/components/RunJSValueEditor.d.ts +1 -0
  22. package/es/flow/components/field-value-variable/DateVariableEditor.d.ts +24 -0
  23. package/es/flow/components/field-value-variable/FieldValueVariableInput.d.ts +31 -0
  24. package/es/flow/components/field-value-variable/dateValue.d.ts +36 -0
  25. package/es/flow/components/field-value-variable/index.d.ts +11 -0
  26. package/es/flow/components/filter/FilterGroup.d.ts +1 -0
  27. package/es/flow/components/filter/VariableFilterItem.d.ts +6 -1
  28. package/es/flow/components/placeholders/BlockPlaceholder.d.ts +1 -0
  29. package/es/flow/index.d.ts +1 -0
  30. package/es/flow/internal/registerDeviceTypeContext.d.ts +10 -0
  31. package/es/flow/internal/utils/operatorSchemaHelper.d.ts +2 -2
  32. package/es/flow/models/actions/UpdateRecordActionUtils.d.ts +8 -2
  33. package/es/flow/models/base/ActionModelCore.d.ts +4 -2
  34. package/es/flow/models/base/BlockGridModel.d.ts +3 -0
  35. package/es/flow/models/base/CollectionBlockModel.d.ts +3 -0
  36. package/es/flow/models/base/PageModel/PageModel.d.ts +12 -0
  37. package/es/flow/models/base/PageModel/PageModelTabBar.d.ts +22 -0
  38. package/es/flow/models/base/PageModel/PageTabModel.d.ts +12 -0
  39. package/es/flow/models/blocks/details/DetailsBlockModel.d.ts +3 -0
  40. package/es/flow/models/blocks/filter-form/FilterFormGridModel.d.ts +1 -0
  41. package/es/flow/models/blocks/filter-form/FilterFormSubmitActionModel.d.ts +3 -1
  42. package/es/flow/models/blocks/form/FormBlockModel.d.ts +7 -0
  43. package/es/flow/models/blocks/form/FormGridModel.d.ts +6 -0
  44. package/es/flow/models/blocks/form/QuickEditFormModel.d.ts +17 -2
  45. package/es/flow/models/blocks/form/value-runtime/rules.d.ts +1 -0
  46. package/es/flow/models/blocks/table/JSColumnModel.d.ts +2 -0
  47. package/es/flow/models/blocks/table/TableBlockModel.d.ts +1 -0
  48. package/es/flow/models/blocks/table/TableColumnModel.d.ts +12 -0
  49. package/es/flow/models/blocks/table/utils.d.ts +1 -0
  50. package/es/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.d.ts +2 -1
  51. package/es/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.d.ts +0 -17
  52. package/es/flow/models/fields/AssociationFieldModel/recordSelectShared.d.ts +41 -0
  53. package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +15 -7
  54. package/es/flow-compat/fieldValidationConstants.d.ts +1 -1
  55. package/es/flow-compat/passwordUtils.d.ts +1 -1
  56. package/es/flow-compat/routeTypes.d.ts +1 -0
  57. package/es/index.d.ts +5 -0
  58. package/es/index.mjs +199 -161
  59. package/es/ui-operation/index.d.ts +15 -0
  60. package/es/ui-operation/ui-operation-codec.d.ts +10 -0
  61. package/es/utils/getRouteRuntimeVersion.d.ts +23 -0
  62. package/es/utils/index.d.ts +1 -0
  63. package/lib/index.js +215 -177
  64. package/lib/locale/languageCodes.js +3 -1
  65. package/package.json +9 -7
  66. package/src/APIClient.ts +90 -7
  67. package/src/Application.tsx +7 -1
  68. package/src/BaseApplication.tsx +11 -6
  69. package/src/__tests__/APIClient.test.tsx +58 -0
  70. package/src/__tests__/app.test.tsx +73 -0
  71. package/src/__tests__/browserChecker.test.ts +61 -0
  72. package/src/__tests__/getRouteRuntimeVersion.test.ts +68 -0
  73. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +35 -2
  74. package/src/__tests__/settings-center.test.tsx +270 -6
  75. package/src/acl/ACLProvider.tsx +2 -0
  76. package/src/acl/__tests__/ACLProvider.test.tsx +92 -0
  77. package/src/collection-field-interface/CollectionFieldInterface.ts +1 -0
  78. package/src/collection-field-interface/CollectionFieldInterfaceManager.ts +1 -0
  79. package/src/collection-manager/field-validation.ts +1 -1
  80. package/src/components/README.md +7 -1
  81. package/src/components/README.zh-CN.md +6 -1
  82. package/src/components/category-tabs/SortableCategoryTabs.tsx +210 -0
  83. package/src/components/category-tabs/index.ts +10 -0
  84. package/src/components/form/JsonTextArea.tsx +4 -0
  85. package/src/components/form/ScanInput/CodeScanner.tsx +79 -35
  86. package/src/components/form/ScanInput/__tests__/CodeScanner.test.tsx +101 -0
  87. package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +284 -11
  88. package/src/components/form/ScanInput/useCodeScanner.ts +332 -24
  89. package/src/components/form/TypedVariableInput.tsx +452 -101
  90. package/src/components/form/__tests__/TypedVariableInput.test.tsx +364 -12
  91. package/src/components/form/filter/CollectionFilter.tsx +4 -0
  92. package/src/components/form/filter/CollectionFilterItem.tsx +36 -9
  93. package/src/components/form/filter/CollectionFilterPanel.tsx +4 -0
  94. package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +55 -0
  95. package/src/components/form/filter/__tests__/useFilterActionProps.test.tsx +95 -0
  96. package/src/components/form/filter/index.ts +2 -0
  97. package/src/components/form/filter/useFilterActionProps.ts +37 -6
  98. package/src/components/form/table/Table.tsx +81 -6
  99. package/src/components/form/table/__tests__/Table.columnWidth.test.tsx +433 -0
  100. package/src/components/index.ts +1 -0
  101. package/src/flow/FlowPage.tsx +9 -1
  102. package/src/flow/__tests__/FlowPage.test.tsx +50 -3
  103. package/src/flow/__tests__/FlowRoute.test.tsx +128 -10
  104. package/src/flow/__tests__/PluginFlowEngine.test.ts +58 -0
  105. package/src/flow/actions/__tests__/actionLinkageRules.forkProps.test.ts +314 -0
  106. package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +102 -0
  107. package/src/flow/actions/__tests__/linkageRules.actionStates.test.ts +33 -0
  108. package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +72 -0
  109. package/src/flow/actions/__tests__/linkageRules.tab.test.ts +171 -0
  110. package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +7 -3
  111. package/src/flow/actions/index.ts +2 -0
  112. package/src/flow/actions/linkageRules.tsx +144 -26
  113. package/src/flow/actions/linkageRulesRefresh.tsx +2 -6
  114. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +0 -1
  115. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +78 -59
  116. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +253 -6
  117. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +228 -0
  118. package/src/flow/common/Markdown/style.ts +4 -0
  119. package/src/flow/components/BlockItemCard.tsx +2 -2
  120. package/src/flow/components/FieldAssignExactDatePicker.tsx +25 -11
  121. package/src/flow/components/FieldAssignValueInput.tsx +72 -584
  122. package/src/flow/components/FlowRoute.tsx +78 -32
  123. package/src/flow/components/RunJSValueEditor.tsx +9 -1
  124. package/src/flow/components/__tests__/FieldAssignValueInput.context.test.tsx +216 -0
  125. package/src/flow/components/field-value-variable/DateVariableEditor.tsx +181 -0
  126. package/src/flow/components/field-value-variable/FieldValueVariableInput.tsx +326 -0
  127. package/src/flow/components/field-value-variable/__tests__/FieldValueVariableInput.test.tsx +380 -0
  128. package/src/flow/components/field-value-variable/dateValue.ts +223 -0
  129. package/src/flow/components/field-value-variable/index.ts +12 -0
  130. package/src/flow/components/filter/FilterGroup.tsx +33 -5
  131. package/src/flow/components/filter/VariableFilterItem.tsx +170 -13
  132. package/src/flow/components/filter/__tests__/FilterGroup.test.tsx +45 -0
  133. package/src/flow/components/filter/__tests__/VariableFilterItem.leftMetaTree.test.tsx +9 -0
  134. package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +195 -1
  135. package/src/flow/components/placeholders/BlockPlaceholder.tsx +70 -23
  136. package/src/flow/components/placeholders/__tests__/BlockPlaceholder.test.tsx +57 -7
  137. package/src/flow/index.ts +3 -0
  138. package/src/flow/internal/registerDeviceTypeContext.ts +39 -0
  139. package/src/flow/internal/utils/operatorSchemaHelper.ts +3 -2
  140. package/src/flow/models/actions/UpdateRecordActionModel.tsx +18 -1
  141. package/src/flow/models/actions/UpdateRecordActionUtils.ts +18 -6
  142. package/src/flow/models/actions/__tests__/UpdateRecordActionModel.test.ts +76 -4
  143. package/src/flow/models/base/ActionModelCore.tsx +11 -2
  144. package/src/flow/models/base/BlockGridModel.tsx +26 -0
  145. package/src/flow/models/base/CollectionBlockModel.tsx +38 -3
  146. package/src/flow/models/base/PageModel/PageModel.tsx +387 -53
  147. package/src/flow/models/base/PageModel/PageModelTabBar.tsx +114 -0
  148. package/src/flow/models/base/PageModel/PageTabModel.tsx +249 -18
  149. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +790 -10
  150. package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.module-isolation.test.tsx +130 -0
  151. package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.test.tsx +118 -0
  152. package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +1075 -7
  153. package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +49 -0
  154. package/src/flow/models/base/__tests__/BlockGridModel.selectSceneActivation.test.ts +124 -0
  155. package/src/flow/models/base/__tests__/CollectionBlockModel.addAppends.test.ts +41 -0
  156. package/src/flow/models/base/__tests__/CollectionBlockModel.initialBeforeRenderRefresh.test.ts +125 -9
  157. package/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx +28 -53
  158. package/src/flow/models/blocks/details/DetailsBlockModel.tsx +8 -3
  159. package/src/flow/models/blocks/details/__tests__/DetailsBlockModel.initialPaginationChangeRefresh.test.ts +32 -1
  160. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +28 -3
  161. package/src/flow/models/blocks/filter-form/FilterFormGridModel.tsx +36 -5
  162. package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +155 -25
  163. package/src/flow/models/blocks/filter-form/FilterFormSubmitActionModel.tsx +57 -4
  164. package/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.onModelCreated.test.ts +174 -1
  165. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +395 -1
  166. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.getFilterValue.test.ts +72 -0
  167. package/src/flow/models/blocks/filter-form/__tests__/FilterFormSubmitActionModel.lifecycle.test.ts +90 -0
  168. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +66 -4
  169. package/src/flow/models/blocks/filter-manager/FilterManager.ts +5 -0
  170. package/src/flow/models/blocks/filter-manager/__tests__/FilterManager.test.ts +40 -0
  171. package/src/flow/models/blocks/form/FormBlockModel.tsx +47 -0
  172. package/src/flow/models/blocks/form/FormGridModel.tsx +4 -0
  173. package/src/flow/models/blocks/form/QuickEditFormModel.tsx +189 -36
  174. package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +8 -2
  175. package/src/flow/models/blocks/form/__tests__/QuickEditFormModel.quickEdit.test.ts +350 -2
  176. package/src/flow/models/blocks/form/__tests__/runJsFormSubmit.test.ts +131 -0
  177. package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +435 -0
  178. package/src/flow/models/blocks/form/value-runtime/rules.ts +67 -14
  179. package/src/flow/models/blocks/form/value-runtime/runtime.ts +43 -19
  180. package/src/flow/models/blocks/js-block/JSBlock.tsx +1 -1
  181. package/src/flow/models/blocks/table/JSColumnModel.tsx +10 -1
  182. package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +2 -1
  183. package/src/flow/models/blocks/table/TableBlockModel.tsx +29 -3
  184. package/src/flow/models/blocks/table/TableColumnModel.tsx +48 -9
  185. package/src/flow/models/blocks/table/__tests__/JSColumnModel.test.tsx +52 -5
  186. package/src/flow/models/blocks/table/__tests__/TableBlockModel.filterReset.test.ts +78 -0
  187. package/src/flow/models/blocks/table/__tests__/TableBlockModel.quickEditRefresh.test.ts +12 -0
  188. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +33 -0
  189. package/src/flow/models/blocks/table/utils.ts +7 -0
  190. package/src/flow/models/fields/AssociationFieldModel/CascadeSelectFieldModel.tsx +33 -1
  191. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +10 -1
  192. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/__tests__/popupContext.test.ts +120 -0
  193. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/actions/PopupSubTableEditActionModel.tsx +10 -2
  194. package/src/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.tsx +24 -1
  195. package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +9 -136
  196. package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +167 -3
  197. package/src/flow/models/fields/AssociationFieldModel/recordSelectShared.tsx +156 -0
  198. package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +149 -0
  199. package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +149 -113
  200. package/src/flow/models/fields/DisplayNumberFieldModel.tsx +1 -1
  201. package/src/flow/models/fields/JsonFieldModel.tsx +31 -8
  202. package/src/flow/models/fields/NumberFieldModel.tsx +1 -1
  203. package/src/flow/models/fields/VariableFieldFormModel.tsx +3 -3
  204. package/src/flow/models/fields/__tests__/DisplayNumberFieldModel.test.ts +33 -0
  205. package/src/flow/models/fields/__tests__/JsonFieldModel.test.ts +82 -0
  206. package/src/flow/models/fields/__tests__/NumberFieldModel.test.tsx +47 -0
  207. package/src/flow/models/fields/__tests__/VariableFieldFormModel.test.tsx +51 -0
  208. package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +26 -10
  209. package/src/flow/models/fields/mobile-components/MobileSelect.tsx +51 -14
  210. package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +139 -13
  211. package/src/flow/utils/dataScopeFormValueClear.ts +4 -3
  212. package/src/flow-compat/fieldValidationConstants.ts +1 -1
  213. package/src/flow-compat/routeTypes.ts +1 -0
  214. package/src/index.ts +5 -0
  215. package/src/layout-manager/LayoutContentRoute.tsx +2 -1
  216. package/src/layout-manager/LayoutRoute.tsx +2 -1
  217. package/src/layout-manager/__tests__/LayoutRoute.test.tsx +87 -1
  218. package/src/locale/languageCodes.ts +3 -1
  219. package/src/nocobase-buildin-plugin/index.tsx +12 -0
  220. package/src/settings-center/AdminSettingsLayout.tsx +23 -2
  221. package/src/ui-operation/index.ts +33 -0
  222. package/src/ui-operation/ui-operation-codec.ts +54 -0
  223. package/src/utils/getRouteRuntimeVersion.ts +185 -0
  224. package/src/utils/index.tsx +1 -0
@@ -8,140 +8,176 @@
8
8
  */
9
9
 
10
10
  import { autorun } from '@formily/reactive';
11
- import { Form } from 'antd';
12
- import { useFlowContext } from '@nocobase/flow-engine';
11
+ import { Form, type FormInstance } from 'antd';
12
+ import { resolveRunJSObjectValues, useFlowContext } from '@nocobase/flow-engine';
13
13
  import { dayjs } from '@nocobase/utils/client';
14
+ import type { ConfigType, Dayjs } from 'dayjs';
14
15
  import { first, last } from 'lodash';
15
- import React, { useEffect, useRef, useState } from 'react';
16
+ import { useCallback, useEffect, useRef, useState } from 'react';
16
17
 
17
18
  type DateLimitProps = {
18
- _minDate?: any;
19
- _maxDate?: any;
20
- currentForm?: any;
19
+ _minDate?: unknown;
20
+ _maxDate?: unknown;
21
+ currentForm?: unknown;
21
22
  };
22
23
 
23
- export function useDateLimit(props: DateLimitProps) {
24
- const ctx = useFlowContext();
25
- const isAntdFormInstance = typeof props.currentForm?.getFieldsValue === 'function';
26
- const currentFormValues = Form.useWatch([], isAntdFormInstance ? props.currentForm : undefined);
27
- const [minDate, setMinDate] = useState<dayjs.Dayjs | null>(null);
28
- const [maxDate, setMaxDate] = useState<dayjs.Dayjs | null>(null);
29
- const [disabledDate, setDisabledDate] = useState<any>(null);
30
- const [disabledTime, setDisabledTime] = useState<any>(null);
31
- const disposeRef = useRef<any>(null);
24
+ type DisabledTimeConfig = {
25
+ disabledHours: () => number[];
26
+ disabledMinutes: (selectedHour: number) => number[];
27
+ disabledSeconds: (selectedHour: number, selectedMinute: number) => number[];
28
+ };
32
29
 
33
- useEffect(() => {
34
- if (disposeRef.current) {
35
- disposeRef.current();
36
- }
30
+ type DisabledDate = (current: Dayjs) => boolean;
31
+ type DisabledTime = (current: Dayjs | null) => DisabledTimeConfig;
37
32
 
38
- disposeRef.current = autorun(() => {
39
- void limitDate();
40
- });
33
+ function isAntdFormInstance(value: unknown): value is FormInstance {
34
+ if (!value || typeof value !== 'object') return false;
35
+ return typeof (value as Partial<FormInstance>).getFieldsValue === 'function';
36
+ }
41
37
 
42
- return () => {
43
- disposeRef.current?.();
44
- };
45
- }, [ctx, currentFormValues, props._maxDate, props._minDate]);
38
+ function parseDateLimitValue(value: unknown): Dayjs | null {
39
+ if (!value) return null;
40
+ const parsed = dayjs(value as ConfigType);
41
+ return parsed.isValid() ? parsed : null;
42
+ }
46
43
 
47
- const limitDate = async () => {
48
- const resolvedParams = await ctx.resolveJsonTemplate({
49
- _minDate: props._minDate,
50
- _maxDate: props._maxDate,
51
- });
44
+ export function useDateLimit(props: DateLimitProps) {
45
+ const ctx = useFlowContext();
46
+ const currentForm = isAntdFormInstance(props.currentForm) ? props.currentForm : undefined;
47
+ const currentFormValues = Form.useWatch([], currentForm);
48
+ const [minDate, setMinDate] = useState<Dayjs | null>(null);
49
+ const [maxDate, setMaxDate] = useState<Dayjs | null>(null);
50
+ const [disabledDate, setDisabledDate] = useState<DisabledDate | null>(null);
51
+ const [disabledTime, setDisabledTime] = useState<DisabledTime | null>(null);
52
+ const evaluationIdRef = useRef(0);
53
+
54
+ const limitDate = useCallback(async () => {
55
+ const evaluationId = ++evaluationIdRef.current;
56
+
57
+ try {
58
+ const runJSResolvedParams = await resolveRunJSObjectValues(ctx, {
59
+ _minDate: props._minDate,
60
+ _maxDate: props._maxDate,
61
+ });
62
+ const resolvedParams = (await ctx.resolveJsonTemplate({
63
+ _minDate: runJSResolvedParams._minDate,
64
+ _maxDate: runJSResolvedParams._maxDate,
65
+ })) as { _minDate?: unknown; _maxDate?: unknown };
66
+
67
+ if (evaluationId !== evaluationIdRef.current) return;
68
+
69
+ const nextMinRaw = Array.isArray(resolvedParams?._minDate)
70
+ ? first(resolvedParams._minDate)
71
+ : resolvedParams?._minDate;
72
+ const nextMaxRaw = Array.isArray(resolvedParams?._maxDate)
73
+ ? last(resolvedParams._maxDate)
74
+ : resolvedParams?._maxDate;
75
+ const nextMinDate = parseDateLimitValue(nextMinRaw);
76
+ const nextMaxDate = parseDateLimitValue(nextMaxRaw);
77
+
78
+ setMinDate(nextMinDate);
79
+ setMaxDate(nextMaxDate);
80
+
81
+ const fullTimeArr = Array.from({ length: 60 }, (_, i) => i);
82
+
83
+ const nextDisabledDate: DisabledDate = (current) => {
84
+ if (!dayjs.isDayjs(current)) return false;
85
+
86
+ const min = nextMinDate?.startOf('day') || null;
87
+ const max = nextMaxDate?.endOf('day') || null;
88
+
89
+ if (min && current.startOf('day').isBefore(min)) {
90
+ return true;
91
+ }
92
+ if (max && current.startOf('day').isAfter(max)) {
93
+ return true;
94
+ }
95
+ return false;
96
+ };
52
97
 
53
- const nextMinRaw = Array.isArray(resolvedParams?._minDate)
54
- ? first(resolvedParams._minDate)
55
- : resolvedParams?._minDate;
56
- const nextMaxRaw = Array.isArray(resolvedParams?._maxDate)
57
- ? last(resolvedParams._maxDate)
58
- : resolvedParams?._maxDate;
59
- const nextMinDate = nextMinRaw ? dayjs(nextMinRaw) : null;
60
- const nextMaxDate = nextMaxRaw ? dayjs(nextMaxRaw) : null;
61
-
62
- setMinDate(nextMinDate?.isValid?.() ? nextMinDate : null);
63
- setMaxDate(nextMaxDate?.isValid?.() ? nextMaxDate : null);
64
-
65
- const fullTimeArr = Array.from({ length: 60 }, (_, i) => i);
66
-
67
- const nextDisabledDate = (current: dayjs.Dayjs) => {
68
- if (!dayjs.isDayjs(current)) return false;
69
-
70
- const min = nextMinDate?.isValid?.() ? nextMinDate.startOf('day') : null;
71
- const max = nextMaxDate?.isValid?.() ? nextMaxDate.endOf('day') : null;
72
-
73
- if (min && current.startOf('day').isBefore(min)) {
74
- return true;
75
- }
76
- if (max && current.startOf('day').isAfter(max)) {
77
- return true;
78
- }
79
- return false;
80
- };
98
+ const nextDisabledTime: DisabledTime = (current) => {
99
+ if (!current || (!nextMinDate && !nextMaxDate)) {
100
+ return { disabledHours: () => [], disabledMinutes: () => [], disabledSeconds: () => [] };
101
+ }
81
102
 
82
- const nextDisabledTime = (current: dayjs.Dayjs) => {
83
- if (!current || (!nextMinDate?.isValid?.() && !nextMaxDate?.isValid?.())) {
84
- return { disabledHours: () => [], disabledMinutes: () => [], disabledSeconds: () => [] };
85
- }
103
+ const isCurrentMinDay = !!nextMinDate && current.isSame(nextMinDate, 'day');
104
+ const isCurrentMaxDay = !!nextMaxDate && current.isSame(nextMaxDate, 'day');
86
105
 
87
- const isCurrentMinDay = !!nextMinDate?.isValid?.() && current.isSame(nextMinDate, 'day');
88
- const isCurrentMaxDay = !!nextMaxDate?.isValid?.() && current.isSame(nextMaxDate, 'day');
106
+ const disabledHours = () => {
107
+ const hours = [];
108
+ if (isCurrentMinDay && nextMinDate) {
109
+ for (let hour = 0; hour < nextMinDate.hour(); hour++) {
110
+ hours.push(hour);
111
+ }
112
+ }
113
+ if (isCurrentMaxDay && nextMaxDate) {
114
+ for (let hour = nextMaxDate.hour() + 1; hour < 24; hour++) {
115
+ hours.push(hour);
116
+ }
117
+ }
118
+ return hours;
119
+ };
89
120
 
90
- const disabledHours = () => {
91
- const hours = [];
92
- if (isCurrentMinDay && nextMinDate) {
93
- for (let hour = 0; hour < nextMinDate.hour(); hour++) {
94
- hours.push(hour);
121
+ const disabledMinutes = (selectedHour: number) => {
122
+ if (isCurrentMinDay && nextMinDate && selectedHour === nextMinDate.hour()) {
123
+ return fullTimeArr.filter((minute) => minute < nextMinDate.minute());
95
124
  }
96
- }
97
- if (isCurrentMaxDay && nextMaxDate) {
98
- for (let hour = nextMaxDate.hour() + 1; hour < 24; hour++) {
99
- hours.push(hour);
125
+ if (isCurrentMaxDay && nextMaxDate && selectedHour === nextMaxDate.hour()) {
126
+ return fullTimeArr.filter((minute) => minute > nextMaxDate.minute());
100
127
  }
101
- }
102
- return hours;
128
+ return [];
129
+ };
130
+
131
+ const disabledSeconds = (selectedHour: number, selectedMinute: number) => {
132
+ if (
133
+ isCurrentMinDay &&
134
+ nextMinDate &&
135
+ selectedHour === nextMinDate.hour() &&
136
+ selectedMinute === nextMinDate.minute()
137
+ ) {
138
+ return fullTimeArr.filter((second) => second < nextMinDate.second());
139
+ }
140
+ if (
141
+ isCurrentMaxDay &&
142
+ nextMaxDate &&
143
+ selectedHour === nextMaxDate.hour() &&
144
+ selectedMinute === nextMaxDate.minute()
145
+ ) {
146
+ return fullTimeArr.filter((second) => second > nextMaxDate.second());
147
+ }
148
+ return [];
149
+ };
150
+
151
+ return {
152
+ disabledHours,
153
+ disabledMinutes,
154
+ disabledSeconds,
155
+ };
103
156
  };
104
157
 
105
- const disabledMinutes = (selectedHour: number) => {
106
- if (isCurrentMinDay && nextMinDate && selectedHour === nextMinDate.hour()) {
107
- return fullTimeArr.filter((minute) => minute < nextMinDate.minute());
108
- }
109
- if (isCurrentMaxDay && nextMaxDate && selectedHour === nextMaxDate.hour()) {
110
- return fullTimeArr.filter((minute) => minute > nextMaxDate.minute());
111
- }
112
- return [];
113
- };
158
+ setDisabledDate(() => nextDisabledDate);
159
+ setDisabledTime(() => nextDisabledTime);
160
+ } catch (error) {
161
+ if (evaluationId !== evaluationIdRef.current) return;
114
162
 
115
- const disabledSeconds = (selectedHour: number, selectedMinute: number) => {
116
- if (
117
- isCurrentMinDay &&
118
- nextMinDate &&
119
- selectedHour === nextMinDate.hour() &&
120
- selectedMinute === nextMinDate.minute()
121
- ) {
122
- return fullTimeArr.filter((second) => second < nextMinDate.second());
123
- }
124
- if (
125
- isCurrentMaxDay &&
126
- nextMaxDate &&
127
- selectedHour === nextMaxDate.hour() &&
128
- selectedMinute === nextMaxDate.minute()
129
- ) {
130
- return fullTimeArr.filter((second) => second > nextMaxDate.second());
131
- }
132
- return [];
133
- };
163
+ ctx.logger?.warn?.({ err: error }, 'Failed to resolve date range limit');
164
+ setMinDate(null);
165
+ setMaxDate(null);
166
+ setDisabledDate(null);
167
+ setDisabledTime(null);
168
+ }
169
+ }, [ctx, props._maxDate, props._minDate]);
134
170
 
135
- return {
136
- disabledHours,
137
- disabledMinutes,
138
- disabledSeconds,
139
- };
140
- };
171
+ useEffect(() => {
172
+ const dispose = autorun(() => {
173
+ limitDate();
174
+ });
141
175
 
142
- setDisabledDate(() => nextDisabledDate);
143
- setDisabledTime(() => nextDisabledTime);
144
- };
176
+ return () => {
177
+ evaluationIdRef.current += 1;
178
+ dispose();
179
+ };
180
+ }, [currentFormValues, limitDate]);
145
181
 
146
182
  return {
147
183
  minDate,
@@ -71,7 +71,7 @@ function formatBigNumberWithSeparator(value, format = '0.00', step = 1, formatSt
71
71
  let number = value;
72
72
 
73
73
  if (formatStyle) {
74
- number = new BigNumber(value).toString();
74
+ number = new BigNumber(value).toFixed();
75
75
  }
76
76
 
77
77
  let formattedNumber = '';
@@ -13,6 +13,22 @@ import JSON5 from 'json5';
13
13
  import React, { useState, useEffect, useRef } from 'react';
14
14
  import { FieldModel } from '../base/FieldModel';
15
15
 
16
+ const jsonValidationRuleMarker = '__jsonSyntaxValidation';
17
+
18
+ interface JsonValidationRule {
19
+ [jsonValidationRuleMarker]: true;
20
+ type: 'any';
21
+ validator?: (_rule: unknown, value: unknown) => Promise<void>;
22
+ }
23
+
24
+ function isRuleObject(rule: unknown): rule is Record<string, unknown> {
25
+ return typeof rule === 'object' && rule !== null;
26
+ }
27
+
28
+ function isJsonValidationRule(rule: unknown): rule is JsonValidationRule {
29
+ return isRuleObject(rule) && rule[jsonValidationRuleMarker] === true;
30
+ }
31
+
16
32
  const jsonCss = css`
17
33
  font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
18
34
  font-size: 14px;
@@ -40,7 +56,7 @@ export const JsonInput = observer((props: any) => {
40
56
  }
41
57
 
42
58
  internalChange.current = false;
43
- }, [value]);
59
+ }, [_JSON, space, value]);
44
60
 
45
61
  // 用户输入时
46
62
  const handleChange = (ev) => {
@@ -91,23 +107,30 @@ JsonFieldModel.registerFlow({
91
107
  key: 'jsonInitSetting',
92
108
  steps: {
93
109
  initValidation: {
94
- handler(ctx, params) {
95
- const rules = ctx.model.parent.props.rules || [];
110
+ handler(ctx) {
111
+ const currentRules = Array.isArray(ctx.model.parent.props.rules) ? ctx.model.parent.props.rules : [];
112
+ const rules = currentRules.filter((rule: unknown) => {
113
+ return (!isRuleObject(rule) || Object.keys(rule).length > 0) && !isJsonValidationRule(rule);
114
+ });
96
115
  // 添加 JSON 语法校验规则
97
116
  rules.push({
98
- validator: (_, value) => {
117
+ // validator 函数序列化后会被丢弃;保留 any 类型可避免持久化规则退化为默认字符串校验。
118
+ type: 'any',
119
+ [jsonValidationRuleMarker]: true,
120
+ validator: (_rule: unknown, value: unknown) => {
99
121
  // 允许空
100
122
  if (value === null || value === undefined || value === '') return Promise.resolve();
101
123
  // 如果已经是对象/数组,直接通过
102
124
  if (typeof value === 'object') return Promise.resolve();
103
125
  try {
104
- JSON.parse(value);
126
+ JSON.parse(String(value));
105
127
  return Promise.resolve();
106
- } catch (err) {
107
- return Promise.reject(ctx.t('Invalid JSON format') + ': ' + err.message);
128
+ } catch (error) {
129
+ const message = error instanceof Error ? error.message : String(error);
130
+ return Promise.reject(ctx.t('Invalid JSON format') + ': ' + message);
108
131
  }
109
132
  },
110
- });
133
+ } satisfies JsonValidationRule);
111
134
  ctx.model.parent.setProps({
112
135
  rules,
113
136
  });
@@ -20,7 +20,7 @@ function toSafeNumber(value) {
20
20
  return value;
21
21
  }
22
22
  if (value > Number.MAX_SAFE_INTEGER || value < Number.MIN_SAFE_INTEGER) {
23
- return new BigNumber(value).toString();
23
+ return new BigNumber(value).toFixed();
24
24
  } else {
25
25
  return Number(value);
26
26
  }
@@ -48,12 +48,12 @@ export class VariableFieldFormModel extends FlowModel {
48
48
  <FormProvider form={this.form}>
49
49
  <FormLayout layout={'vertical'}>
50
50
  {this.mapSubModels('fields', (field) => {
51
- // 确保字段模型具备稳定的 id/name,便于依赖路径的组件(如公式字段)正确解析
51
+ // Avoid conflicting with the DOM nodeName property when rendering the temporary input.
52
52
  const init = field?.getStepParams?.('fieldSettings', 'init') || {};
53
53
  const fp = init?.fieldPath as string | undefined;
54
54
  if (fp) {
55
- const namePath = fp.includes('.') ? fp.split('.') : [fp];
56
- const toSet: any = {};
55
+ const namePath = (fp === 'nodeName' ? '__nb_variable_field_nodeName' : fp).split('.');
56
+ const toSet: Record<string, unknown> = {};
57
57
  if (!field?.props?.id) toSet.id = namePath;
58
58
  if (!field?.props?.name) toSet.name = namePath;
59
59
  if (Object.keys(toSet).length) field?.setProps?.(toSet);
@@ -0,0 +1,33 @@
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 { describe, expect, it } from 'vitest';
11
+ import { formatNumber } from '../DisplayNumberFieldModel';
12
+
13
+ describe('formatNumber', () => {
14
+ it('formats a 30-digit decimal without scientific notation or precision loss', () => {
15
+ expect(
16
+ formatNumber({
17
+ value: '123456789012345678901234567890',
18
+ formatStyle: 'normal',
19
+ step: '1',
20
+ }),
21
+ ).toBe('123,456,789,012,345,678,901,234,567,890');
22
+ });
23
+
24
+ it('formats the 22-digit value from the reported v2 scenario', () => {
25
+ expect(
26
+ formatNumber({
27
+ value: '1234567890123458152112',
28
+ formatStyle: 'normal',
29
+ step: '1',
30
+ }),
31
+ ).toBe('1,234,567,890,123,458,152,112');
32
+ });
33
+ });
@@ -0,0 +1,82 @@
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, FlowModel } from '@nocobase/flow-engine';
11
+ import { describe, expect, it } from 'vitest';
12
+ import { JsonFieldModel } from '../JsonFieldModel';
13
+
14
+ interface ValidationRule {
15
+ required?: boolean;
16
+ type?: string;
17
+ validator?: (_rule: unknown, value: unknown) => Promise<void>;
18
+ }
19
+
20
+ function createModels(rules: ValidationRule[]) {
21
+ const engine = new FlowEngine();
22
+ engine.registerModels({ JsonFieldModel });
23
+ const parent = engine.createModel<FlowModel<{ subModels: { field: JsonFieldModel } }>>({
24
+ use: FlowModel,
25
+ uid: 'json-form-item',
26
+ props: { rules },
27
+ subModels: {
28
+ field: {
29
+ use: JsonFieldModel,
30
+ uid: 'json-field',
31
+ },
32
+ },
33
+ });
34
+
35
+ return { field: parent.subModels.field, parent };
36
+ }
37
+
38
+ function initializeJsonValidation(field: JsonFieldModel) {
39
+ const step = field.getFlow('jsonInitSetting')?.steps.initValidation;
40
+ if (!step) {
41
+ throw new Error('JSON validation step is not registered');
42
+ }
43
+ step.handler(field.context, {});
44
+ }
45
+
46
+ describe('JsonFieldModel validation', () => {
47
+ it('replaces legacy empty rules with one serializable JSON validation rule', async () => {
48
+ const { field, parent } = createModels([{}, { required: true }]);
49
+
50
+ initializeJsonValidation(field);
51
+
52
+ const rules = parent.props.rules as ValidationRule[];
53
+ expect(rules).toHaveLength(2);
54
+ expect(rules[0]).toEqual({ required: true });
55
+ expect(rules[1]).toMatchObject({ type: 'any' });
56
+ expect(rules[1].validator).toBeTypeOf('function');
57
+ if (!rules[1].validator) {
58
+ throw new Error('JSON validator is missing');
59
+ }
60
+ await expect(rules[1].validator({}, { name: 'NocoBase' })).resolves.toBeUndefined();
61
+ await expect(rules[1].validator({}, '{"name":')).rejects.toContain('Invalid JSON format');
62
+
63
+ const persistedRules = JSON.parse(JSON.stringify(parent.serialize())).props.rules as ValidationRule[];
64
+ expect(persistedRules).toHaveLength(2);
65
+ expect(persistedRules[1]).toMatchObject({ type: 'any' });
66
+ expect(persistedRules[1].validator).toBeUndefined();
67
+ });
68
+
69
+ it('re-registers the runtime validator without duplicating its persisted placeholder', () => {
70
+ const { field, parent } = createModels([{}]);
71
+ initializeJsonValidation(field);
72
+ const persistedRules = JSON.parse(JSON.stringify(parent.serialize())).props.rules as ValidationRule[];
73
+ parent.setProps({ rules: persistedRules });
74
+
75
+ initializeJsonValidation(field);
76
+
77
+ const rules = parent.props.rules as ValidationRule[];
78
+ expect(rules).toHaveLength(1);
79
+ expect(rules[0]).toMatchObject({ type: 'any' });
80
+ expect(rules[0].validator).toBeTypeOf('function');
81
+ });
82
+ });
@@ -0,0 +1,47 @@
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 { fireEvent, render, screen } from '@testing-library/react';
11
+ import React from 'react';
12
+ import { describe, expect, it, vi } from 'vitest';
13
+ import { InputNumberField } from '../NumberFieldModel';
14
+
15
+ describe('InputNumberField', () => {
16
+ it('keeps the existing number value type for regular input', () => {
17
+ const onChange = vi.fn();
18
+
19
+ render(<InputNumberField stringMode onChange={onChange} />);
20
+ fireEvent.change(screen.getByRole('spinbutton'), { target: { value: '123' } });
21
+
22
+ expect(onChange).toHaveBeenLastCalledWith(123);
23
+ expect(onChange).not.toHaveBeenCalledWith('123');
24
+ });
25
+
26
+ it('preserves a high-precision decimal string in string mode', () => {
27
+ const onChange = vi.fn();
28
+ const value = '123456789012345678901234567890';
29
+
30
+ render(<InputNumberField stringMode onChange={onChange} />);
31
+ fireEvent.change(screen.getByRole('spinbutton'), { target: { value } });
32
+
33
+ expect(onChange).toHaveBeenLastCalledWith(value);
34
+ expect(screen.getByRole('spinbutton')).toHaveValue(value);
35
+ });
36
+
37
+ it('does not switch to scientific notation at the 22-digit boundary', () => {
38
+ const onChange = vi.fn();
39
+ const value = '1234567890123458152112';
40
+
41
+ render(<InputNumberField stringMode onChange={onChange} />);
42
+ fireEvent.change(screen.getByRole('spinbutton'), { target: { value } });
43
+
44
+ expect(onChange).toHaveBeenLastCalledWith(value);
45
+ expect(onChange).not.toHaveBeenCalledWith('1.234567890123458152112e+21');
46
+ });
47
+ });
@@ -0,0 +1,51 @@
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, FlowEngineProvider } from '@nocobase/flow-engine';
11
+ import { render, screen, waitFor } from '@testing-library/react';
12
+ import React from 'react';
13
+ import { describe, expect, it } from 'vitest';
14
+ import { InputFieldModel } from '../InputFieldModel';
15
+ import { VariableFieldFormModel } from '../VariableFieldFormModel';
16
+
17
+ describe('VariableFieldFormModel', () => {
18
+ it('uses DOM-safe name and id for temporary controls created from reserved form property names', async () => {
19
+ const engine = new FlowEngine();
20
+ engine.registerModels({ InputFieldModel, VariableFieldFormModel });
21
+ const model = engine.createModel<VariableFieldFormModel>({
22
+ use: 'VariableFieldFormModel',
23
+ subModels: {
24
+ fields: [
25
+ {
26
+ use: 'InputFieldModel',
27
+ stepParams: {
28
+ fieldSettings: {
29
+ init: {
30
+ fieldPath: 'nodeName',
31
+ },
32
+ },
33
+ },
34
+ },
35
+ ],
36
+ },
37
+ });
38
+ const field = model.subModels.fields[0];
39
+
40
+ render(<FlowEngineProvider engine={engine}>{model.render()}</FlowEngineProvider>);
41
+
42
+ const input = await screen.findByRole('textbox');
43
+ await waitFor(() => {
44
+ expect(field.props.name).toEqual(['__nb_variable_field_nodeName']);
45
+ });
46
+ expect(input).toHaveAttribute('name', '__nb_variable_field_nodeName');
47
+ expect(input).toHaveAttribute('id', '__nb_variable_field_nodeName');
48
+ expect(field.props.id).toEqual(['__nb_variable_field_nodeName']);
49
+ expect(field.getStepParams('fieldSettings', 'init')).toEqual({ fieldPath: 'nodeName' });
50
+ });
51
+ });