@nocobase/client-v2 2.2.0-beta.5 → 2.2.0-beta.7

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 (121) hide show
  1. package/es/PluginSettingsManager.d.ts +33 -0
  2. package/es/RouteRepository.d.ts +21 -6
  3. package/es/components/form/JsonTextArea.d.ts +2 -1
  4. package/es/components/form/VariableJsonTextArea.d.ts +19 -0
  5. package/es/components/form/index.d.ts +1 -0
  6. package/es/components/index.d.ts +1 -0
  7. package/es/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.d.ts +2 -0
  8. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.d.ts +2 -1
  9. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +2 -0
  10. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.d.ts +3 -2
  11. package/es/flow/admin-shell/admin-layout/AdminLayoutSlotModels.d.ts +4 -0
  12. package/es/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.d.ts +6 -0
  13. package/es/flow/components/FieldAssignRulesEditor.d.ts +3 -1
  14. package/es/flow/models/base/GridModel.d.ts +1 -1
  15. package/es/flow/models/blocks/filter-form/FilterFormBlockModel.d.ts +5 -0
  16. package/es/flow/models/blocks/form/FormBlockModel.d.ts +4 -0
  17. package/es/flow/models/blocks/form/value-runtime/rules.d.ts +21 -0
  18. package/es/flow/models/blocks/form/value-runtime/runtime.d.ts +21 -0
  19. package/es/flow/models/blocks/form/value-runtime/types.d.ts +2 -2
  20. package/es/flow-compat/FieldValidation.d.ts +2 -1
  21. package/es/index.d.ts +2 -1
  22. package/es/index.mjs +145 -119
  23. package/lib/index.js +148 -122
  24. package/package.json +7 -7
  25. package/src/PluginSettingsManager.ts +53 -0
  26. package/src/RouteRepository.ts +126 -24
  27. package/src/__tests__/PluginSettingsManager.test.ts +13 -0
  28. package/src/__tests__/RouteRepository.test.ts +216 -0
  29. package/src/__tests__/exports.test.ts +16 -0
  30. package/src/__tests__/plugin-manager.test.tsx +44 -2
  31. package/src/__tests__/settings-center.test.tsx +40 -1
  32. package/src/components/form/JsonTextArea.tsx +21 -11
  33. package/src/components/form/VariableJsonTextArea.tsx +175 -0
  34. package/src/components/form/__tests__/JsonTextArea.test.tsx +43 -0
  35. package/src/components/form/__tests__/VariableJsonTextArea.test.tsx +86 -0
  36. package/src/components/form/index.tsx +1 -0
  37. package/src/components/index.ts +1 -0
  38. package/src/flow/__tests__/FlowRoute.test.tsx +443 -5
  39. package/src/flow/actions/__tests__/dataScopeFilter.test.ts +58 -0
  40. package/src/flow/actions/__tests__/fieldLinkageRules.scopeDepth.test.ts +150 -1
  41. package/src/flow/actions/__tests__/linkageAssignField.legacy.test.ts +135 -0
  42. package/src/flow/actions/__tests__/linkageRules.hiddenSync.restore.test.ts +19 -5
  43. package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +92 -0
  44. package/src/flow/actions/customVariable.tsx +1 -2
  45. package/src/flow/actions/dataScopeFilter.ts +10 -0
  46. package/src/flow/actions/linkageRules.tsx +122 -34
  47. package/src/flow/actions/runjs.tsx +2 -14
  48. package/src/flow/actions/validation.tsx +62 -30
  49. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +33 -10
  50. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.test.tsx +310 -0
  51. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +44 -8
  52. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.ts +5 -3
  53. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +61 -9
  54. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +71 -8
  55. package/src/flow/admin-shell/admin-layout/AdminLayoutModel.tsx +1 -1
  56. package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +21 -8
  57. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +188 -0
  58. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +99 -0
  59. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +166 -2
  60. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +20 -0
  61. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +33 -5
  62. package/src/flow/components/DefaultValue.tsx +1 -2
  63. package/src/flow/components/FieldAssignRulesEditor.tsx +128 -21
  64. package/src/flow/components/FlowRoute.tsx +56 -11
  65. package/src/flow/components/__tests__/FieldAssignRulesEditor.test.tsx +508 -4
  66. package/src/flow/components/__tests__/fieldAssignOptions.test.ts +151 -3
  67. package/src/flow/components/code-editor/__tests__/useCodeRunner.test.tsx +2 -17
  68. package/src/flow/components/code-editor/hooks/useCodeRunner.ts +2 -14
  69. package/src/flow/components/code-editor/runjsDiagnostics.ts +8 -45
  70. package/src/flow/components/fieldAssignOptions.ts +155 -27
  71. package/src/flow/models/actions/JSActionModel.tsx +2 -7
  72. package/src/flow/models/actions/JSCollectionActionModel.tsx +2 -7
  73. package/src/flow/models/actions/JSItemActionModel.tsx +2 -7
  74. package/src/flow/models/actions/JSRecordActionModel.tsx +2 -7
  75. package/src/flow/models/base/ActionModel.tsx +12 -1
  76. package/src/flow/models/base/GridModel.tsx +38 -7
  77. package/src/flow/models/base/PageModel/PageModel.tsx +4 -1
  78. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +28 -4
  79. package/src/flow/models/base/__tests__/ActionModel.test.ts +83 -0
  80. package/src/flow/models/base/__tests__/GridModel.dragSnapshotContainer.test.ts +239 -1
  81. package/src/flow/models/base/__tests__/transformRowsToSingleColumn.test.ts +48 -0
  82. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +31 -3
  83. package/src/flow/models/blocks/filter-form/FilterFormJSActionModel.tsx +2 -7
  84. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +77 -0
  85. package/src/flow/models/blocks/form/EditFormModel.tsx +1 -0
  86. package/src/flow/models/blocks/form/FormBlockModel.tsx +7 -0
  87. package/src/flow/models/blocks/form/JSFormActionModel.tsx +2 -7
  88. package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +17 -0
  89. package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +880 -102
  90. package/src/flow/models/blocks/form/value-runtime/rules.ts +445 -13
  91. package/src/flow/models/blocks/form/value-runtime/runtime.ts +257 -13
  92. package/src/flow/models/blocks/form/value-runtime/types.ts +2 -2
  93. package/src/flow/models/blocks/js-block/JSBlock.tsx +2 -7
  94. package/src/flow/models/blocks/table/JSColumnModel.tsx +1 -9
  95. package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +1 -1
  96. package/src/flow/models/blocks/table/TableBlockModel.tsx +13 -3
  97. package/src/flow/models/blocks/table/__tests__/TableActionsColumnModel.test.tsx +54 -0
  98. package/src/flow/models/blocks/table/__tests__/TableBlockModel.mobileSettingsButtons.test.tsx +78 -0
  99. package/src/flow/models/fields/AssociationFieldModel/AssociationFieldModel.tsx +1 -1
  100. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +29 -6
  101. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +13 -1
  102. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableFieldModel.reset.test.ts +180 -0
  103. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +6 -4
  104. package/src/flow/models/fields/AssociationFieldModel/__tests__/AssociationFieldModel.updateAssociation.test.ts +72 -0
  105. package/src/flow/models/fields/InputFieldModel.tsx +14 -22
  106. package/src/flow/models/fields/JSEditableFieldModel.tsx +2 -11
  107. package/src/flow/models/fields/JSFieldModel.tsx +2 -7
  108. package/src/flow/models/fields/JSItemModel.tsx +2 -14
  109. package/src/flow/models/fields/__tests__/InputFieldModel.test.tsx +17 -0
  110. package/src/flow/models/topbar/TopbarActionModel.tsx +93 -10
  111. package/src/flow-compat/FieldValidation.tsx +122 -60
  112. package/src/flow-compat/Popover.tsx +43 -4
  113. package/src/flow-compat/__tests__/Popover.test.tsx +34 -0
  114. package/src/index.ts +8 -1
  115. package/src/layout-manager/__tests__/LayoutRoute.test.tsx +41 -1
  116. package/src/nocobase-buildin-plugin/index.tsx +0 -2
  117. package/src/settings-center/AdminSettingsLayout.tsx +3 -0
  118. package/src/settings-center/SystemSettingsPage.tsx +0 -1
  119. package/src/settings-center/plugin-manager/PluginCard.tsx +2 -2
  120. package/src/settings-center/plugin-manager/index.tsx +3 -0
  121. package/src/settings-center/utils.tsx +0 -6
@@ -14,7 +14,7 @@ import { message } from 'antd';
14
14
  import { AdminSettingsLayoutModel as ClientV2AdminSettingsLayoutModel } from '../settings-center';
15
15
  import { AdminSettingsLayoutModel as ClientV1AdminSettingsLayoutModel } from '../../../client/src/pm/AdminSettingsLayoutModel';
16
16
  import { NocoBaseBuildInPlugin } from '../nocobase-buildin-plugin';
17
- import { matchSettingsRoute } from '../settings-center/utils';
17
+ import { matchSettingsRoute, sortTopLevelSettings } from '../settings-center/utils';
18
18
 
19
19
  class TestAclPlugin extends Plugin {
20
20
  async load() {
@@ -168,6 +168,12 @@ describe('settings center', () => {
168
168
  });
169
169
  });
170
170
 
171
+ it('should sort system-settings with other top-level settings by normal ordering', () => {
172
+ const settings = [{ name: 'system-settings' }, { name: 'api-keys' }, { name: 'backups' }] as any;
173
+
174
+ expect(sortTopLevelSettings(settings).map((item) => item.name)).toEqual(['api-keys', 'backups', 'system-settings']);
175
+ });
176
+
171
177
  it('should redirect /admin/settings to system-settings by default', async () => {
172
178
  const app = createMockClient({
173
179
  plugins: [NocoBaseBuildInPlugin, TestAclPlugin],
@@ -354,6 +360,39 @@ describe('settings center', () => {
354
360
  expect(screen.getByRole('menuitem', { name: 'Menu ACL Demo' })).toBeInTheDocument();
355
361
  });
356
362
 
363
+ it('should allow the settings sidebar menu to scroll independently', async () => {
364
+ class ManySettingsPlugin extends Plugin {
365
+ async load() {
366
+ for (let index = 0; index < 30; index += 1) {
367
+ this.pluginSettingsManager.addMenuItem({
368
+ key: `scroll-demo-${index}`,
369
+ title: `Scroll demo ${index}`,
370
+ });
371
+ this.pluginSettingsManager.addPageTabItem({
372
+ menuKey: `scroll-demo-${index}`,
373
+ key: 'index',
374
+ title: `Scroll demo ${index}`,
375
+ Component: () => <div>{`Scroll demo page ${index}`}</div>,
376
+ });
377
+ }
378
+ }
379
+ }
380
+
381
+ const app = createMockClient({
382
+ plugins: [NocoBaseBuildInPlugin, TestAclPlugin, ManySettingsPlugin],
383
+ router: { type: 'memory', initialEntries: ['/admin/settings/scroll-demo-29'] },
384
+ });
385
+ mockAdminRuntime(app);
386
+
387
+ await renderApp(app);
388
+ await waitForGetRequests(app, ['/auth:check', 'roles:check']);
389
+
390
+ expect(await screen.findByText('Scroll demo page 29')).toBeInTheDocument();
391
+
392
+ const sidebar = screen.getByRole('menuitem', { name: 'Scroll demo 29' }).closest('.ant-layout-sider');
393
+ expect(sidebar).toHaveStyle({ overflowY: 'auto' });
394
+ });
395
+
357
396
  it('should save system settings through systemSettings:put', async () => {
358
397
  const app = createMockClient({
359
398
  plugins: [NocoBaseBuildInPlugin, TestAclPlugin],
@@ -10,8 +10,9 @@
10
10
  import { css, cx } from '@emotion/css';
11
11
  import { Input, Typography } from 'antd';
12
12
  import type { TextAreaProps } from 'antd/es/input';
13
+ import type { TextAreaRef } from 'antd/es/input/TextArea';
13
14
  import JSON5 from 'json5';
14
- import React, { useCallback, useEffect, useMemo, useState } from 'react';
15
+ import React, { useCallback, useEffect, useMemo, useState, type ChangeEvent, type FocusEvent } from 'react';
15
16
 
16
17
  export interface JsonTextAreaProps extends Omit<TextAreaProps, 'value' | 'onChange'> {
17
18
  value?: unknown;
@@ -36,14 +37,14 @@ function stringifyJsonValue(value: unknown, json: typeof JSON | typeof JSON5, sp
36
37
  json.parse(value);
37
38
  return value;
38
39
  } catch {
39
- return json.stringify(value, undefined, space);
40
+ return json.stringify(value, undefined, space) ?? '';
40
41
  }
41
42
  }
42
43
 
43
- return json.stringify(value, undefined, space);
44
+ return json.stringify(value, undefined, space) ?? '';
44
45
  }
45
46
 
46
- export const JsonTextArea = React.memo((props: JsonTextAreaProps) => {
47
+ const JsonTextAreaComponent = React.forwardRef<TextAreaRef, JsonTextAreaProps>((props, ref) => {
47
48
  const {
48
49
  value,
49
50
  onChange,
@@ -74,11 +75,8 @@ export const JsonTextArea = React.memo((props: JsonTextAreaProps) => {
74
75
  [json],
75
76
  );
76
77
 
77
- const handleChange = useCallback(
78
- (event: React.ChangeEvent<HTMLTextAreaElement>) => {
79
- const nextText = event.target.value;
80
- setText(nextText);
81
-
78
+ const validateText = useCallback(
79
+ (nextText: string) => {
82
80
  try {
83
81
  parseText(nextText);
84
82
  setError(undefined);
@@ -89,12 +87,21 @@ export const JsonTextArea = React.memo((props: JsonTextAreaProps) => {
89
87
  [parseText],
90
88
  );
91
89
 
90
+ const handleChange = useCallback(
91
+ (event: ChangeEvent<HTMLTextAreaElement>) => {
92
+ const nextText = event.target.value;
93
+ setText(nextText);
94
+ validateText(nextText);
95
+ },
96
+ [validateText],
97
+ );
98
+
92
99
  const handleBlur = useCallback(
93
- (event: React.FocusEvent<HTMLTextAreaElement>) => {
100
+ (event: FocusEvent<HTMLTextAreaElement>) => {
94
101
  try {
95
102
  const parsed = parseText(event.target.value);
96
103
  setError(undefined);
97
- setText(parsed == null ? '' : json.stringify(parsed, undefined, space));
104
+ setText(parsed == null ? '' : json.stringify(parsed, undefined, space) ?? '');
98
105
  onChange?.(parsed);
99
106
  } catch (err) {
100
107
  setError(err instanceof Error ? err.message : String(err));
@@ -111,6 +118,7 @@ export const JsonTextArea = React.memo((props: JsonTextAreaProps) => {
111
118
  <>
112
119
  <Input.TextArea
113
120
  {...textAreaProps}
121
+ ref={ref}
114
122
  value={text}
115
123
  onChange={handleChange}
116
124
  onBlur={handleBlur}
@@ -126,4 +134,6 @@ export const JsonTextArea = React.memo((props: JsonTextAreaProps) => {
126
134
  );
127
135
  });
128
136
 
137
+ export const JsonTextArea = React.memo(JsonTextAreaComponent);
138
+
129
139
  JsonTextArea.displayName = 'JsonTextArea';
@@ -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
+ });
@@ -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
+ });
@@ -19,3 +19,4 @@ export * from './RemoteSelect';
19
19
  export * from './ScanInput';
20
20
  export * from './TypedVariableInput';
21
21
  export * from './VariableInput';
22
+ export * from './VariableJsonTextArea';
@@ -12,6 +12,7 @@ export * from './BlankComponent';
12
12
  export * from './form/table/dnd';
13
13
  export * from './form';
14
14
  export * from './Icon';
15
+ export * from './KeepAlive';
15
16
  export * from './PoweredBy';
16
17
  export * from './RouterContextCleaner';
17
18
  export * from './SwitchLanguage';