@nocobase/client-v2 2.2.0-alpha.1 → 2.2.0-alpha.3

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 (220) hide show
  1. package/es/PluginSettingsManager.d.ts +33 -0
  2. package/es/RouteRepository.d.ts +21 -6
  3. package/es/components/category-tabs/SortableCategoryTabs.d.ts +55 -0
  4. package/es/components/category-tabs/index.d.ts +9 -0
  5. package/es/components/form/JsonTextArea.d.ts +2 -1
  6. package/es/components/form/TypedVariableInput.d.ts +22 -4
  7. package/es/components/form/VariableJsonTextArea.d.ts +19 -0
  8. package/es/components/form/filter/CollectionFilterItem.d.ts +11 -1
  9. package/es/components/form/filter/index.d.ts +2 -0
  10. package/es/components/form/index.d.ts +1 -0
  11. package/es/components/index.d.ts +2 -0
  12. package/es/flow/actions/afterSuccess.d.ts +8 -1
  13. package/es/flow/actions/index.d.ts +1 -1
  14. package/es/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.d.ts +2 -0
  15. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.d.ts +2 -1
  16. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +2 -0
  17. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.d.ts +3 -2
  18. package/es/flow/admin-shell/admin-layout/AdminLayoutSlotModels.d.ts +5 -0
  19. package/es/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.d.ts +6 -0
  20. package/es/flow/components/FieldAssignRulesEditor.d.ts +3 -1
  21. package/es/flow/components/FieldAssignValueInput.d.ts +2 -0
  22. package/es/flow/index.d.ts +1 -0
  23. package/es/flow/models/base/GridModel.d.ts +1 -1
  24. package/es/flow/models/blocks/filter-form/FilterFormBlockModel.d.ts +5 -0
  25. package/es/flow/models/blocks/form/FormBlockModel.d.ts +4 -0
  26. package/es/flow/models/blocks/form/submitHandler.d.ts +1 -1
  27. package/es/flow/models/blocks/form/value-runtime/rules.d.ts +21 -0
  28. package/es/flow/models/blocks/form/value-runtime/runtime.d.ts +30 -0
  29. package/es/flow/models/blocks/form/value-runtime/types.d.ts +2 -2
  30. package/es/flow/models/blocks/js-block/JSBlock.d.ts +1 -0
  31. package/es/flow/models/blocks/table/TableActionsColumnModel.d.ts +1 -0
  32. package/es/flow/models/blocks/table/TableBlockModel.d.ts +1 -0
  33. package/es/flow/models/blocks/table/dragSort/dragSortHooks.d.ts +1 -1
  34. package/es/flow/models/blocks/table/dragSort/dragSortSettings.d.ts +2 -1
  35. package/es/flow/models/blocks/table/dragSort/dragSortUtils.d.ts +6 -4
  36. package/es/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.d.ts +2 -0
  37. package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +6 -0
  38. package/es/flow/resolveViewParamsToViewList.d.ts +1 -1
  39. package/es/flow/utils/dataScopeRowSnapshot.d.ts +59 -0
  40. package/es/flow/utils/dateTimeDisplayProps.d.ts +49 -0
  41. package/es/flow/utils/formValueDeps.d.ts +32 -0
  42. package/es/flow-compat/FieldValidation.d.ts +2 -1
  43. package/es/index.d.ts +2 -1
  44. package/es/index.mjs +233 -137
  45. package/lib/index.js +234 -138
  46. package/package.json +7 -7
  47. package/src/PluginSettingsManager.ts +53 -0
  48. package/src/RouteRepository.ts +126 -24
  49. package/src/__tests__/PluginSettingsManager.test.ts +13 -0
  50. package/src/__tests__/RouteRepository.test.ts +216 -0
  51. package/src/__tests__/browserChecker.test.ts +61 -0
  52. package/src/__tests__/exports.test.ts +16 -0
  53. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +199 -0
  54. package/src/__tests__/plugin-manager.test.tsx +44 -2
  55. package/src/__tests__/settings-center.test.tsx +40 -1
  56. package/src/collection-manager/field-configure.ts +1 -1
  57. package/src/collection-manager/interfaces/id.ts +1 -1
  58. package/src/collection-manager/interfaces/m2m.tsx +2 -2
  59. package/src/collection-manager/interfaces/m2o.tsx +2 -2
  60. package/src/collection-manager/interfaces/nanoid.ts +1 -1
  61. package/src/collection-manager/interfaces/o2m.tsx +2 -2
  62. package/src/collection-manager/interfaces/obo.tsx +2 -2
  63. package/src/collection-manager/interfaces/oho.tsx +2 -2
  64. package/src/collection-manager/interfaces/properties/index.ts +2 -2
  65. package/src/collection-manager/interfaces/uuid.ts +1 -1
  66. package/src/components/AppComponents.tsx +19 -3
  67. package/src/components/README.md +7 -1
  68. package/src/components/README.zh-CN.md +6 -1
  69. package/src/components/category-tabs/SortableCategoryTabs.tsx +210 -0
  70. package/src/components/category-tabs/index.ts +10 -0
  71. package/src/components/form/JsonTextArea.tsx +21 -11
  72. package/src/components/form/TypedVariableInput.tsx +416 -93
  73. package/src/components/form/VariableJsonTextArea.tsx +175 -0
  74. package/src/components/form/__tests__/JsonTextArea.test.tsx +43 -0
  75. package/src/components/form/__tests__/TypedVariableInput.test.tsx +268 -9
  76. package/src/components/form/__tests__/VariableJsonTextArea.test.tsx +86 -0
  77. package/src/components/form/filter/CollectionFilterItem.tsx +32 -7
  78. package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +38 -0
  79. package/src/components/form/filter/index.ts +2 -0
  80. package/src/components/form/index.tsx +1 -0
  81. package/src/components/index.ts +2 -0
  82. package/src/flow/__tests__/FlowRoute.test.tsx +443 -5
  83. package/src/flow/__tests__/getKey.test.ts +7 -0
  84. package/src/flow/__tests__/resolveViewParamsToViewList.test.ts +34 -0
  85. package/src/flow/actions/__tests__/afterSuccess.test.ts +73 -0
  86. package/src/flow/actions/__tests__/dataScopeFilter.test.ts +58 -0
  87. package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +1022 -0
  88. package/src/flow/actions/__tests__/fieldLinkageRules.scopeDepth.test.ts +150 -1
  89. package/src/flow/actions/__tests__/linkageAssignField.legacy.test.ts +135 -0
  90. package/src/flow/actions/__tests__/linkageRules.hiddenSync.restore.test.ts +19 -5
  91. package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +92 -0
  92. package/src/flow/actions/__tests__/openView.defineProps.route.test.tsx +104 -0
  93. package/src/flow/actions/__tests__/subFormFieldLinkageRules.inputArgs.test.ts +2 -2
  94. package/src/flow/actions/afterSuccess.tsx +142 -3
  95. package/src/flow/actions/customVariable.tsx +1 -2
  96. package/src/flow/actions/dataScopeFilter.ts +10 -0
  97. package/src/flow/actions/dateTimeFormat.tsx +42 -28
  98. package/src/flow/actions/index.ts +1 -1
  99. package/src/flow/actions/linkageRules.tsx +123 -35
  100. package/src/flow/actions/linkageRulesFormValueRefresh.ts +22 -130
  101. package/src/flow/actions/openView.tsx +38 -4
  102. package/src/flow/actions/runjs.tsx +2 -14
  103. package/src/flow/actions/validation.tsx +62 -30
  104. package/src/flow/admin-shell/BaseLayoutModel.tsx +25 -3
  105. package/src/flow/admin-shell/BaseLayoutRouteCoordinator.ts +5 -2
  106. package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +13 -0
  107. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +34 -10
  108. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.test.tsx +310 -0
  109. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +44 -8
  110. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.ts +5 -3
  111. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +61 -9
  112. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +71 -8
  113. package/src/flow/admin-shell/admin-layout/AdminLayoutModel.tsx +1 -1
  114. package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +23 -9
  115. package/src/flow/admin-shell/admin-layout/HelpLite.tsx +1 -3
  116. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +188 -0
  117. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +99 -0
  118. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +82 -0
  119. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +166 -2
  120. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +20 -0
  121. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +33 -5
  122. package/src/flow/components/DefaultValue.tsx +1 -2
  123. package/src/flow/components/DynamicFlowsIcon.tsx +447 -126
  124. package/src/flow/components/FieldAssignRulesEditor.tsx +128 -21
  125. package/src/flow/components/FieldAssignValueInput.tsx +10 -5
  126. package/src/flow/components/FlowRoute.tsx +56 -11
  127. package/src/flow/components/__tests__/DynamicFlowsIcon.test.tsx +148 -2
  128. package/src/flow/components/__tests__/FieldAssignRulesEditor.test.tsx +508 -4
  129. package/src/flow/components/__tests__/fieldAssignOptions.test.ts +151 -3
  130. package/src/flow/components/code-editor/__tests__/useCodeRunner.test.tsx +2 -17
  131. package/src/flow/components/code-editor/hooks/useCodeRunner.ts +2 -14
  132. package/src/flow/components/code-editor/runjsDiagnostics.ts +8 -45
  133. package/src/flow/components/fieldAssignOptions.ts +155 -27
  134. package/src/flow/flows/editMarkdownFlow.tsx +1 -1
  135. package/src/flow/getViewDiffAndUpdateHidden.tsx +1 -0
  136. package/src/flow/index.ts +2 -1
  137. package/src/flow/internal/utils/operatorSchemaHelper.ts +15 -1
  138. package/src/flow/models/actions/JSActionModel.tsx +2 -7
  139. package/src/flow/models/actions/JSCollectionActionModel.tsx +2 -7
  140. package/src/flow/models/actions/JSItemActionModel.tsx +2 -7
  141. package/src/flow/models/actions/JSRecordActionModel.tsx +2 -7
  142. package/src/flow/models/base/ActionModel.tsx +12 -1
  143. package/src/flow/models/base/ActionModelCore.tsx +6 -4
  144. package/src/flow/models/base/GridModel.tsx +38 -7
  145. package/src/flow/models/base/PageModel/PageModel.tsx +4 -1
  146. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +28 -4
  147. package/src/flow/models/base/__tests__/ActionModel.test.ts +83 -0
  148. package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +37 -0
  149. package/src/flow/models/base/__tests__/GridModel.dragSnapshotContainer.test.ts +239 -1
  150. package/src/flow/models/base/__tests__/transformRowsToSingleColumn.test.ts +48 -0
  151. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +31 -3
  152. package/src/flow/models/blocks/filter-form/FilterFormJSActionModel.tsx +2 -7
  153. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +77 -0
  154. package/src/flow/models/blocks/filter-form/fields/FilterFormCustomFieldModel.tsx +1 -1
  155. package/src/flow/models/blocks/filter-manager/flow-actions/__tests__/customizeFilterRender.test.tsx +56 -1
  156. package/src/flow/models/blocks/form/EditFormModel.tsx +1 -0
  157. package/src/flow/models/blocks/form/FormActionModel.tsx +1 -1
  158. package/src/flow/models/blocks/form/FormBlockModel.tsx +7 -0
  159. package/src/flow/models/blocks/form/JSFormActionModel.tsx +2 -7
  160. package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +17 -0
  161. package/src/flow/models/blocks/form/__tests__/submitHandler.test.ts +54 -1
  162. package/src/flow/models/blocks/form/submitHandler.ts +17 -3
  163. package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +940 -75
  164. package/src/flow/models/blocks/form/value-runtime/rules.ts +445 -13
  165. package/src/flow/models/blocks/form/value-runtime/runtime.ts +348 -13
  166. package/src/flow/models/blocks/form/value-runtime/types.ts +2 -2
  167. package/src/flow/models/blocks/js-block/JSBlock.tsx +225 -9
  168. package/src/flow/models/blocks/js-block/__tests__/JSBlockModel.test.tsx +150 -0
  169. package/src/flow/models/blocks/table/JSColumnModel.tsx +1 -9
  170. package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +36 -13
  171. package/src/flow/models/blocks/table/TableBlockModel.tsx +38 -17
  172. package/src/flow/models/blocks/table/TableColumnModel.tsx +36 -3
  173. package/src/flow/models/blocks/table/__tests__/TableActionsColumnModel.test.tsx +111 -1
  174. package/src/flow/models/blocks/table/__tests__/TableBlockModel.dragSort.test.ts +127 -0
  175. package/src/flow/models/blocks/table/__tests__/TableBlockModel.mobileSettingsButtons.test.tsx +78 -0
  176. package/src/flow/models/blocks/table/__tests__/TableBlockModel.rowSelection.test.tsx +1 -0
  177. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +159 -1
  178. package/src/flow/models/blocks/table/dragSort/dragSortHooks.tsx +28 -17
  179. package/src/flow/models/blocks/table/dragSort/dragSortSettings.ts +13 -6
  180. package/src/flow/models/blocks/table/dragSort/dragSortUtils.ts +15 -2
  181. package/src/flow/models/fields/AssociationFieldModel/AssociationFieldModel.tsx +1 -1
  182. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +29 -6
  183. package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +16 -4
  184. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +141 -19
  185. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +13 -1
  186. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableColumnModel.rowRecord.test.ts +197 -0
  187. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableFieldModel.reset.test.ts +180 -0
  188. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +6 -4
  189. package/src/flow/models/fields/AssociationFieldModel/__tests__/AssociationFieldModel.updateAssociation.test.ts +72 -0
  190. package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +23 -0
  191. package/src/flow/models/fields/ClickableFieldModel.tsx +5 -0
  192. package/src/flow/models/fields/DisplayDateTimeFieldModel.tsx +29 -1
  193. package/src/flow/models/fields/InputFieldModel.tsx +40 -2
  194. package/src/flow/models/fields/JSEditableFieldModel.tsx +2 -11
  195. package/src/flow/models/fields/JSFieldModel.tsx +2 -7
  196. package/src/flow/models/fields/JSItemModel.tsx +2 -14
  197. package/src/flow/models/fields/SelectFieldModel.tsx +6 -4
  198. package/src/flow/models/fields/TextareaFieldModel.tsx +49 -3
  199. package/src/flow/models/fields/__tests__/DisplayDateTimeFieldModel.test.tsx +96 -0
  200. package/src/flow/models/fields/__tests__/InputFieldModel.test.tsx +117 -1
  201. package/src/flow/models/fields/__tests__/SelectFieldModel.test.tsx +43 -0
  202. package/src/flow/models/fields/__tests__/TextareaFieldModel.test.tsx +100 -0
  203. package/src/flow/models/topbar/TopbarActionModel.tsx +93 -10
  204. package/src/flow/resolveViewParamsToViewList.tsx +11 -3
  205. package/src/flow/utils/__tests__/dateTimeFormat.test.ts +258 -0
  206. package/src/flow/utils/dataScopeFormValueClear.ts +218 -81
  207. package/src/flow/utils/dataScopeRowSnapshot.ts +616 -0
  208. package/src/flow/utils/dateTimeDisplayProps.ts +135 -0
  209. package/src/flow/utils/formValueDeps.ts +170 -0
  210. package/src/flow-compat/FieldValidation.tsx +122 -60
  211. package/src/flow-compat/Popover.tsx +43 -4
  212. package/src/flow-compat/__tests__/Popover.test.tsx +34 -0
  213. package/src/index.ts +8 -1
  214. package/src/layout-manager/__tests__/LayoutRoute.test.tsx +41 -1
  215. package/src/nocobase-buildin-plugin/index.tsx +44 -14
  216. package/src/settings-center/AdminSettingsLayout.tsx +10 -2
  217. package/src/settings-center/SystemSettingsPage.tsx +1 -2
  218. package/src/settings-center/plugin-manager/PluginCard.tsx +2 -2
  219. package/src/settings-center/plugin-manager/index.tsx +3 -0
  220. package/src/settings-center/utils.tsx +0 -6
@@ -0,0 +1,175 @@
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 { css, cx } from '@emotion/css';
11
+ import {
12
+ FlowContextSelector,
13
+ formatPathToValue as formatFlowPathToValue,
14
+ useFlowContext,
15
+ type MetaTreeNode,
16
+ } from '@nocobase/flow-engine';
17
+ import { Button, theme } from 'antd';
18
+ import type { TextAreaRef } from 'antd/es/input/TextArea';
19
+ import React, { useCallback, useMemo, useRef } from 'react';
20
+ import { JsonTextArea, type JsonTextAreaProps } from './JsonTextArea';
21
+ import { useFilteredMetaTree } from './VariableInput';
22
+
23
+ export interface VariableJsonTextAreaProps extends JsonTextAreaProps {
24
+ namespaces?: string[];
25
+ extraNodes?: MetaTreeNode[];
26
+ metaTree?: MetaTreeNode[] | (() => MetaTreeNode[] | Promise<MetaTreeNode[]>);
27
+ formatPathToValue?: (meta: MetaTreeNode) => string | undefined;
28
+ }
29
+
30
+ export const VariableJsonTextArea = React.memo((props: VariableJsonTextAreaProps) => {
31
+ const {
32
+ value,
33
+ onChange,
34
+ space = 2,
35
+ json5 = false,
36
+ showError = true,
37
+ className,
38
+ status,
39
+ onBlur,
40
+ namespaces,
41
+ extraNodes,
42
+ metaTree,
43
+ formatPathToValue: customFormatPathToValue,
44
+ ...textAreaProps
45
+ } = props;
46
+ const { token } = theme.useToken();
47
+ const flowCtx = useFlowContext();
48
+ const filteredMetaTree = useFilteredMetaTree({ namespaces, extraNodes });
49
+ const ref = useRef<TextAreaRef>(null);
50
+
51
+ const insertAtCaret = useCallback((valueToInsert: string) => {
52
+ const textArea = ref.current?.resizableTextArea?.textArea;
53
+ if (!textArea) return;
54
+
55
+ const start = textArea.selectionStart ?? textArea.value.length;
56
+ const end = textArea?.selectionEnd ?? start;
57
+ const nextText = textArea.value.slice(0, start) + valueToInsert + textArea.value.slice(end);
58
+ const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set;
59
+
60
+ nativeInputValueSetter?.call(textArea, nextText);
61
+ textArea.dispatchEvent(new Event('input', { bubbles: true }));
62
+
63
+ requestAnimationFrame(() => {
64
+ const nextPosition = start + valueToInsert.length;
65
+ textArea.setSelectionRange(start, nextPosition);
66
+ textArea.focus();
67
+ });
68
+ }, []);
69
+
70
+ const handleVariableSelected = useCallback(
71
+ (nextValue: string) => {
72
+ if (nextValue) {
73
+ insertAtCaret(nextValue);
74
+ }
75
+ },
76
+ [insertAtCaret],
77
+ );
78
+
79
+ const selectorMetaTree = useMemo(
80
+ () => metaTree ?? filteredMetaTree ?? (() => flowCtx.getPropertyMetaTree?.() ?? []),
81
+ [filteredMetaTree, flowCtx, metaTree],
82
+ );
83
+
84
+ const formatPathToValue = useMemo(() => {
85
+ if (!customFormatPathToValue) {
86
+ return;
87
+ }
88
+ return (meta: MetaTreeNode) => customFormatPathToValue(meta) ?? formatFlowPathToValue(meta);
89
+ }, [customFormatPathToValue]);
90
+
91
+ const wrapperClassName = useMemo(
92
+ () => css`
93
+ position: relative;
94
+ width: 100%;
95
+
96
+ .ant-input {
97
+ width: 100%;
98
+ }
99
+ `,
100
+ [],
101
+ );
102
+
103
+ const textAreaClassName = useMemo(
104
+ () => css`
105
+ font-size: ${token.fontSizeSM}px;
106
+ font-family: ${token.fontFamilyCode};
107
+ `,
108
+ [token.fontFamilyCode, token.fontSizeSM],
109
+ );
110
+
111
+ const selectorClassName = useMemo(
112
+ () => css`
113
+ position: absolute;
114
+ inset-block-start: 0;
115
+ inset-inline-end: 0;
116
+ z-index: 1;
117
+ line-height: 0;
118
+
119
+ .ant-btn {
120
+ font-size: ${token.fontSizeSM}px;
121
+ }
122
+ `,
123
+ [token.fontSizeSM],
124
+ );
125
+
126
+ const buttonClassName = useMemo(
127
+ () => css`
128
+ &:not(:hover) {
129
+ border-inline-end-color: transparent;
130
+ border-block-start-color: transparent;
131
+ background-color: transparent;
132
+ }
133
+ `,
134
+ [],
135
+ );
136
+
137
+ return (
138
+ <div className={wrapperClassName}>
139
+ <JsonTextArea
140
+ {...textAreaProps}
141
+ ref={ref}
142
+ value={value}
143
+ onChange={onChange}
144
+ space={space}
145
+ json5={json5}
146
+ showError={showError}
147
+ className={cx(textAreaClassName, className)}
148
+ status={status}
149
+ onBlur={onBlur}
150
+ disabled={textAreaProps.disabled}
151
+ />
152
+ <div className={selectorClassName}>
153
+ <FlowContextSelector
154
+ metaTree={selectorMetaTree}
155
+ disabled={textAreaProps.disabled}
156
+ formatPathToValue={formatPathToValue}
157
+ onChange={handleVariableSelected}
158
+ >
159
+ <Button
160
+ aria-label="variable-json-switcher"
161
+ disabled={textAreaProps.disabled}
162
+ className={buttonClassName}
163
+ style={{ fontStyle: 'italic', fontFamily: token.fontFamilyCode }}
164
+ >
165
+ x
166
+ </Button>
167
+ </FlowContextSelector>
168
+ </div>
169
+ </div>
170
+ );
171
+ });
172
+
173
+ VariableJsonTextArea.displayName = 'VariableJsonTextArea';
174
+
175
+ export const VariableJSON = VariableJsonTextArea;
@@ -0,0 +1,43 @@
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 { JsonTextArea } from '../JsonTextArea';
14
+
15
+ describe('JsonTextArea', () => {
16
+ it('formats object values and emits parsed JSON on blur', async () => {
17
+ const handleChange = vi.fn();
18
+
19
+ render(<JsonTextArea value={{ foo: 'bar' }} onChange={handleChange} />);
20
+
21
+ const textArea = await screen.findByRole('textbox');
22
+ expect(textArea).toHaveValue('{\n "foo": "bar"\n}');
23
+
24
+ fireEvent.change(textArea, { target: { value: '{"foo":"baz"}' } });
25
+ fireEvent.blur(textArea);
26
+
27
+ expect(handleChange).toHaveBeenCalledWith({ foo: 'baz' });
28
+ expect(textArea).toHaveValue('{\n "foo": "baz"\n}');
29
+ });
30
+
31
+ it('shows JSON parse errors and does not emit invalid values', async () => {
32
+ const handleChange = vi.fn();
33
+
34
+ render(<JsonTextArea value={{}} onChange={handleChange} />);
35
+
36
+ const textArea = await screen.findByRole('textbox');
37
+ fireEvent.change(textArea, { target: { value: '{' } });
38
+ fireEvent.blur(textArea);
39
+
40
+ expect(handleChange).not.toHaveBeenCalled();
41
+ expect(await screen.findByText(/JSON/)).toBeInTheDocument();
42
+ });
43
+ });
@@ -7,7 +7,7 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import { FlowContext, FlowContextProvider } from '@nocobase/flow-engine';
10
+ import { FlowContext, FlowContextProvider, type MetaTreeNode } from '@nocobase/flow-engine';
11
11
  import { fireEvent, render, screen, waitFor } from '@testing-library/react';
12
12
  import React from 'react';
13
13
  import { describe, expect, it, vi } from 'vitest';
@@ -64,6 +64,7 @@ describe('TypedVariableInput - constant rendering', () => {
64
64
  const item = screen.getByText('True');
65
65
  expect(item).toBeInTheDocument();
66
66
  });
67
+ expect(screen.getByRole('button', { name: 'variable-switcher' }).className).not.toContain('ant-btn-primary');
67
68
  });
68
69
 
69
70
  it('renders the Null placeholder when value=null and nullable=true', async () => {
@@ -77,6 +78,69 @@ describe('TypedVariableInput - constant rendering', () => {
77
78
  const nullInput = await screen.findByPlaceholderText('<Null>');
78
79
  expect(nullInput).toBeInTheDocument();
79
80
  expect(nullInput.getAttribute('readonly')).not.toBeNull();
81
+ expect(screen.getByRole('button', { name: 'variable-switcher' }).className).not.toContain('ant-btn-primary');
82
+ });
83
+
84
+ it('defaults undefined to the first constant type', async () => {
85
+ const ctx = createContextWithEnv();
86
+ const handleChange = vi.fn();
87
+ renderWithCtx(
88
+ ctx,
89
+ <TypedVariableInput
90
+ value={undefined}
91
+ types={['string', 'number']}
92
+ namespaces={['$env']}
93
+ nullable
94
+ onChange={handleChange}
95
+ />,
96
+ );
97
+
98
+ expect(screen.queryByPlaceholderText('<Null>')).toBeNull();
99
+ expect(screen.getByRole('textbox')).toBeInTheDocument();
100
+ await waitFor(() => {
101
+ expect(handleChange).toHaveBeenCalledWith('');
102
+ });
103
+ });
104
+
105
+ it('can still opt out to keep the null placeholder for undefined', async () => {
106
+ const ctx = createContextWithEnv();
107
+ renderWithCtx(
108
+ ctx,
109
+ <TypedVariableInput
110
+ value={undefined}
111
+ types={['string', 'number']}
112
+ namespaces={['$env']}
113
+ nullable
114
+ defaultToFirstConstantTypeWhenUndefined={false}
115
+ onChange={() => undefined}
116
+ />,
117
+ );
118
+
119
+ const nullInput = await screen.findByPlaceholderText('<Null>');
120
+ expect(nullInput).toBeInTheDocument();
121
+ });
122
+
123
+ it('highlights the defaulted first constant type when the switcher opens', async () => {
124
+ const ctx = createContextWithEnv();
125
+ renderWithCtx(
126
+ ctx,
127
+ <TypedVariableInput
128
+ value={undefined}
129
+ types={['string', 'number']}
130
+ namespaces={['$env']}
131
+ nullable
132
+ onChange={() => undefined}
133
+ />,
134
+ );
135
+
136
+ fireEvent.click(screen.getByRole('button', { name: 'variable-switcher' }));
137
+
138
+ await waitFor(() => {
139
+ const constant = screen.getAllByText('Constant')[0];
140
+ const string = screen.getAllByText('String')[0];
141
+ expect(constant.closest('.ant-cascader-menu-item-active')).not.toBeNull();
142
+ expect(string.closest('.ant-cascader-menu-item-active')).not.toBeNull();
143
+ });
80
144
  });
81
145
  });
82
146
 
@@ -99,10 +163,10 @@ describe('TypedVariableInput - variable rendering', () => {
99
163
  });
100
164
  });
101
165
 
102
- it('clears back to null when the close button is clicked (nullable=true)', async () => {
166
+ it('clears back to default-of-first-type when the close button is clicked (nullable=true)', async () => {
103
167
  const ctx = createContextWithEnv();
104
168
  const handleChange = vi.fn();
105
- renderWithCtx(
169
+ const { container } = renderWithCtx(
106
170
  ctx,
107
171
  <TypedVariableInput
108
172
  value="{{$env.SMTP_PORT}}"
@@ -112,15 +176,17 @@ describe('TypedVariableInput - variable rendering', () => {
112
176
  onChange={handleChange}
113
177
  />,
114
178
  );
115
- const clear = await screen.findByRole('button', { name: 'icon-close' });
116
- fireEvent.click(clear);
117
- expect(handleChange).toHaveBeenCalledWith(null);
179
+ const clear = container.querySelector('button.clear-button') as HTMLButtonElement | null;
180
+ expect(clear).not.toBeNull();
181
+ expect(clear).toHaveClass('clear-button');
182
+ fireEvent.click(clear as HTMLButtonElement);
183
+ expect(handleChange).toHaveBeenCalledWith(0);
118
184
  });
119
185
 
120
186
  it('clears back to default-of-first-type when nullable=false', async () => {
121
187
  const ctx = createContextWithEnv();
122
188
  const handleChange = vi.fn();
123
- renderWithCtx(
189
+ const { container } = renderWithCtx(
124
190
  ctx,
125
191
  <TypedVariableInput
126
192
  value="{{$env.SMTP_PORT}}"
@@ -130,12 +196,205 @@ describe('TypedVariableInput - variable rendering', () => {
130
196
  onChange={handleChange}
131
197
  />,
132
198
  );
133
- const clear = await screen.findByRole('button', { name: 'icon-close' });
134
- fireEvent.click(clear);
199
+ const clear = container.querySelector('button.clear-button') as HTMLButtonElement | null;
200
+ expect(clear).not.toBeNull();
201
+ fireEvent.click(clear as HTMLButtonElement);
135
202
  expect(handleChange).toHaveBeenCalledWith(0);
136
203
  });
137
204
  });
138
205
 
206
+ describe('TypedVariableInput - object / JSON constant', () => {
207
+ it('renders a JSON textarea for an object value when types include object', async () => {
208
+ const ctx = createContextWithEnv();
209
+ renderWithCtx(
210
+ ctx,
211
+ <TypedVariableInput value={{ a: 1 }} types={['object']} namespaces={['$env']} onChange={() => undefined} />,
212
+ );
213
+ // The object is stringified into a textarea.
214
+ const textarea = await screen.findByDisplayValue(/"a": 1/);
215
+ expect(textarea.tagName.toLowerCase()).toBe('textarea');
216
+ });
217
+
218
+ it('parses valid JSON on blur and emits the parsed object', async () => {
219
+ const ctx = createContextWithEnv();
220
+ const handleChange = vi.fn();
221
+ renderWithCtx(
222
+ ctx,
223
+ <TypedVariableInput value={{}} types={['object']} namespaces={['$env']} onChange={handleChange} />,
224
+ );
225
+ const textarea = await screen.findByDisplayValue('{}');
226
+ fireEvent.change(textarea, { target: { value: '{"x": 42}' } });
227
+ fireEvent.blur(textarea);
228
+ expect(handleChange).toHaveBeenLastCalledWith({ x: 42 });
229
+ });
230
+
231
+ it('shows an error live on change (before blur) and does not emit on invalid JSON', async () => {
232
+ const ctx = createContextWithEnv();
233
+ const handleChange = vi.fn();
234
+ renderWithCtx(
235
+ ctx,
236
+ <TypedVariableInput value={{}} types={['object']} namespaces={['$env']} onChange={handleChange} />,
237
+ );
238
+ const textarea = await screen.findByDisplayValue('{}');
239
+ // Typing an invalid value surfaces the error immediately — no blur needed (mirrors v1's `Json`, which validates on
240
+ // every change).
241
+ fireEvent.change(textarea, { target: { value: '{ not json' } });
242
+ await waitFor(() => {
243
+ // The raw `JSON.parse` error message is shown (matching v1), e.g. "Expected property name or '}' in JSON at
244
+ // position …".
245
+ expect(screen.getByText(/Expected property name/i)).toBeInTheDocument();
246
+ });
247
+ // The value is only emitted on blur, and never for an invalid value.
248
+ fireEvent.blur(textarea);
249
+ expect(handleChange).not.toHaveBeenCalled();
250
+ });
251
+
252
+ it('defaults to {} when the Constant > JSON type is picked', async () => {
253
+ const ctx = createContextWithEnv();
254
+ const handleChange = vi.fn();
255
+ renderWithCtx(
256
+ ctx,
257
+ <TypedVariableInput value={null} types={['object']} namespaces={['$env']} onChange={handleChange} />,
258
+ );
259
+ fireEvent.click(screen.getByRole('button', { name: 'variable-switcher' }));
260
+ // Constant submenu → JSON leaf.
261
+ fireEvent.click(await screen.findByText('Constant'));
262
+ fireEvent.click(await screen.findByText('JSON'));
263
+ expect(handleChange).toHaveBeenCalledWith({});
264
+ });
265
+ });
266
+
267
+ describe('TypedVariableInput - injected metaTree', () => {
268
+ // A bare context with no global `$env` — the injected tree is the only source.
269
+ function createBareContext() {
270
+ const ctx = new FlowContext();
271
+ (ctx as any).t = (key: string) => key;
272
+ return ctx;
273
+ }
274
+
275
+ // Mirrors a workflow node-result tree: a root whose children load lazily.
276
+ function makeLazyTree(loadChildren: () => Promise<MetaTreeNode[]>): MetaTreeNode[] {
277
+ return [
278
+ {
279
+ name: '$jobsMapByNodeKey',
280
+ title: 'Node result',
281
+ type: 'object',
282
+ paths: ['$jobsMapByNodeKey'],
283
+ children: loadChildren,
284
+ },
285
+ ];
286
+ }
287
+
288
+ it('renders the injected tree in the switcher, not the global one', async () => {
289
+ const ctx = createBareContext();
290
+ const metaTree: MetaTreeNode[] = [
291
+ { name: 'n1', title: 'Approval node', type: 'object', paths: ['$jobsMapByNodeKey', 'n1'] },
292
+ ];
293
+ renderWithCtx(ctx, <TypedVariableInput value={null} metaTree={metaTree} onChange={() => undefined} />);
294
+ fireEvent.click(screen.getByRole('button', { name: 'variable-switcher' }));
295
+ await waitFor(() => {
296
+ expect(screen.getByText('Approval node')).toBeInTheDocument();
297
+ });
298
+ });
299
+
300
+ it('lazily resolves function children when a node is expanded', async () => {
301
+ const ctx = createBareContext();
302
+ const loadChildren = vi.fn(async () => [
303
+ { name: 'status', title: 'Status', type: 'string', paths: ['$jobsMapByNodeKey', 'n1', 'status'] },
304
+ ]);
305
+ const metaTree = makeLazyTree(loadChildren);
306
+ renderWithCtx(ctx, <TypedVariableInput value={null} metaTree={metaTree} onChange={() => undefined} />);
307
+
308
+ fireEvent.click(screen.getByRole('button', { name: 'variable-switcher' }));
309
+ const root = await screen.findByText('Node result');
310
+ // Expanding the lazy node triggers loadData → loadMetaTreeChildren.
311
+ fireEvent.click(root);
312
+ await waitFor(() => {
313
+ expect(loadChildren).toHaveBeenCalled();
314
+ expect(screen.getByText('Status')).toBeInTheDocument();
315
+ });
316
+ });
317
+
318
+ it('surfaces lazy children on the FIRST expansion (no stuck-loading regression)', async () => {
319
+ const ctx = createBareContext();
320
+ // A deferred promise so loading→resolved is a real two-phase transition, reproducing the rc-cascader
321
+ // reference-cache bug where the first expand stayed stuck on the spinner until the column was reopened.
322
+ let resolveChildren: (nodes: MetaTreeNode[]) => void = () => undefined;
323
+ const childrenPromise = new Promise<MetaTreeNode[]>((resolve) => {
324
+ resolveChildren = resolve;
325
+ });
326
+ const loadChildren = vi.fn(() => childrenPromise);
327
+ const metaTree = makeLazyTree(loadChildren);
328
+ renderWithCtx(ctx, <TypedVariableInput value={null} metaTree={metaTree} onChange={() => undefined} />);
329
+
330
+ fireEvent.click(screen.getByRole('button', { name: 'variable-switcher' }));
331
+ const root = await screen.findByText('Node result');
332
+ fireEvent.click(root);
333
+ expect(loadChildren).toHaveBeenCalledTimes(1);
334
+ // Resolve after the click — the child must appear without reopening.
335
+ resolveChildren([
336
+ { name: 'secret', title: 'SMTP_HOST', type: 'string', paths: ['$jobsMapByNodeKey', 'n1', 'secret'] },
337
+ ]);
338
+ await waitFor(() => {
339
+ expect(screen.getByText('SMTP_HOST')).toBeInTheDocument();
340
+ });
341
+ });
342
+
343
+ it('emits the selected variable as a {{ ... }} expression with no inner spaces', async () => {
344
+ const ctx = createBareContext();
345
+ const handleChange = vi.fn();
346
+ const metaTree: MetaTreeNode[] = [
347
+ { name: 'n1', title: 'Approval node', type: 'string', paths: ['$jobsMapByNodeKey', 'n1'] },
348
+ ];
349
+ renderWithCtx(ctx, <TypedVariableInput value={null} metaTree={metaTree} onChange={handleChange} />);
350
+ fireEvent.click(screen.getByRole('button', { name: 'variable-switcher' }));
351
+ const leaf = await screen.findByText('Approval node');
352
+ fireEvent.click(leaf);
353
+ await waitFor(() => {
354
+ expect(handleChange).toHaveBeenCalledWith('{{$jobsMapByNodeKey.n1}}');
355
+ });
356
+ });
357
+
358
+ it('preloads a saved variable label across a lazy level (deep label not dropped on mount)', async () => {
359
+ const ctx = createBareContext();
360
+ // The query-node case: a saved reference `$jobsMapByNodeKey.n1.role` points below `n1`, whose children are a lazy
361
+ // thunk (relation not yet expanded). On mount the tag must show the full path "Query / Role", not stop at "Query" —
362
+ // the component walks the saved path resolving each lazy level. (Regression: reopening a saved condition showed
363
+ // only "节点数据 / 查询数据" and dropped "角色标识".)
364
+ const loadRoleFields = vi.fn(async () => [
365
+ { name: 'role', title: 'Role', type: 'string', paths: ['$jobsMapByNodeKey', 'n1', 'role'] },
366
+ ]);
367
+ const metaTree: MetaTreeNode[] = [
368
+ {
369
+ name: '$jobsMapByNodeKey',
370
+ title: 'Node result',
371
+ type: 'object',
372
+ paths: ['$jobsMapByNodeKey'],
373
+ children: [
374
+ {
375
+ name: 'n1',
376
+ title: 'Query',
377
+ type: 'object',
378
+ paths: ['$jobsMapByNodeKey', 'n1'],
379
+ children: loadRoleFields,
380
+ },
381
+ ],
382
+ },
383
+ ];
384
+ renderWithCtx(
385
+ ctx,
386
+ <TypedVariableInput value="{{$jobsMapByNodeKey.n1.role}}" metaTree={metaTree} onChange={() => undefined} />,
387
+ );
388
+ const tag = screen.getByRole('button', { name: 'variable-tag' });
389
+ // Preload resolves the lazy level so the deep label appears without expanding.
390
+ await waitFor(() => {
391
+ expect(loadRoleFields).toHaveBeenCalled();
392
+ expect(tag.textContent).toContain('Role');
393
+ });
394
+ expect(tag.textContent).toContain('Query');
395
+ });
396
+ });
397
+
139
398
  describe('TypedVariableInput - editor onChange propagation', () => {
140
399
  it('forwards numeric edits via onChange', async () => {
141
400
  const ctx = createContextWithEnv();
@@ -0,0 +1,86 @@
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 { FlowContext, FlowContextProvider } from '@nocobase/flow-engine';
11
+ import { fireEvent, render, screen, waitFor } from '@testing-library/react';
12
+ import React from 'react';
13
+ import { describe, expect, it, vi } from 'vitest';
14
+ import { VariableJsonTextArea } from '../VariableJsonTextArea';
15
+
16
+ function createContextWithEnv() {
17
+ const ctx = new FlowContext();
18
+ (ctx as unknown as { t: (key: string) => string }).t = (key: string) => key;
19
+
20
+ ctx.defineProperty('$env', {
21
+ value: { API_KEY: 'secret' },
22
+ meta: {
23
+ title: 'Env',
24
+ type: 'object',
25
+ properties: {
26
+ API_KEY: { title: 'API Key', type: 'string' },
27
+ },
28
+ },
29
+ });
30
+
31
+ return ctx;
32
+ }
33
+
34
+ function renderWithCtx(ctx: FlowContext, node: React.ReactNode) {
35
+ return render(<FlowContextProvider context={ctx}>{node}</FlowContextProvider>);
36
+ }
37
+
38
+ describe('VariableJsonTextArea', () => {
39
+ it('formats object values and emits parsed JSON on blur', async () => {
40
+ const ctx = createContextWithEnv();
41
+ const handleChange = vi.fn();
42
+
43
+ renderWithCtx(ctx, <VariableJsonTextArea value={{ foo: 'bar' }} onChange={handleChange} />);
44
+
45
+ const textArea = await screen.findByRole('textbox');
46
+ expect(textArea).toHaveValue('{\n "foo": "bar"\n}');
47
+
48
+ fireEvent.change(textArea, { target: { value: '{"foo":"baz"}' } });
49
+ fireEvent.blur(textArea);
50
+
51
+ expect(handleChange).toHaveBeenCalledWith({ foo: 'baz' });
52
+ expect(textArea).toHaveValue('{\n "foo": "baz"\n}');
53
+ });
54
+
55
+ it('shows JSON parse errors and does not emit invalid values', async () => {
56
+ const ctx = createContextWithEnv();
57
+ const handleChange = vi.fn();
58
+
59
+ renderWithCtx(ctx, <VariableJsonTextArea value={{}} onChange={handleChange} />);
60
+
61
+ const textArea = await screen.findByRole('textbox');
62
+ fireEvent.change(textArea, { target: { value: '{' } });
63
+ fireEvent.blur(textArea);
64
+
65
+ expect(handleChange).not.toHaveBeenCalled();
66
+ expect(await screen.findByText(/JSON/)).toBeInTheDocument();
67
+ });
68
+
69
+ it('inserts selected variables with the client-v2 template format', async () => {
70
+ const ctx = createContextWithEnv();
71
+
72
+ renderWithCtx(ctx, <VariableJsonTextArea value={null} namespaces={['$env']} onChange={() => undefined} />);
73
+
74
+ fireEvent.click(await screen.findByRole('button', { name: 'variable-json-switcher' }));
75
+
76
+ await waitFor(() => {
77
+ fireEvent.click(screen.getByText('Env'));
78
+ });
79
+
80
+ await waitFor(() => {
81
+ fireEvent.click(screen.getByText('API Key'));
82
+ });
83
+
84
+ expect(await screen.findByRole('textbox')).toHaveValue('{{ ctx.$env.API_KEY }}');
85
+ });
86
+ });