@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.
- package/es/PluginSettingsManager.d.ts +33 -0
- package/es/RouteRepository.d.ts +21 -6
- package/es/components/category-tabs/SortableCategoryTabs.d.ts +55 -0
- package/es/components/category-tabs/index.d.ts +9 -0
- package/es/components/form/JsonTextArea.d.ts +2 -1
- package/es/components/form/TypedVariableInput.d.ts +22 -4
- package/es/components/form/VariableJsonTextArea.d.ts +19 -0
- package/es/components/form/filter/CollectionFilterItem.d.ts +11 -1
- package/es/components/form/filter/index.d.ts +2 -0
- package/es/components/form/index.d.ts +1 -0
- package/es/components/index.d.ts +2 -0
- package/es/flow/actions/afterSuccess.d.ts +8 -1
- package/es/flow/actions/index.d.ts +1 -1
- package/es/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.d.ts +2 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.d.ts +2 -1
- package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +2 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.d.ts +3 -2
- package/es/flow/admin-shell/admin-layout/AdminLayoutSlotModels.d.ts +5 -0
- package/es/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.d.ts +6 -0
- package/es/flow/components/FieldAssignRulesEditor.d.ts +3 -1
- package/es/flow/components/FieldAssignValueInput.d.ts +2 -0
- package/es/flow/index.d.ts +1 -0
- package/es/flow/models/base/GridModel.d.ts +1 -1
- package/es/flow/models/blocks/filter-form/FilterFormBlockModel.d.ts +5 -0
- package/es/flow/models/blocks/form/FormBlockModel.d.ts +4 -0
- package/es/flow/models/blocks/form/submitHandler.d.ts +1 -1
- package/es/flow/models/blocks/form/value-runtime/rules.d.ts +21 -0
- package/es/flow/models/blocks/form/value-runtime/runtime.d.ts +30 -0
- package/es/flow/models/blocks/form/value-runtime/types.d.ts +2 -2
- package/es/flow/models/blocks/js-block/JSBlock.d.ts +1 -0
- package/es/flow/models/blocks/table/TableActionsColumnModel.d.ts +1 -0
- package/es/flow/models/blocks/table/TableBlockModel.d.ts +1 -0
- package/es/flow/models/blocks/table/dragSort/dragSortHooks.d.ts +1 -1
- package/es/flow/models/blocks/table/dragSort/dragSortSettings.d.ts +2 -1
- package/es/flow/models/blocks/table/dragSort/dragSortUtils.d.ts +6 -4
- package/es/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.d.ts +2 -0
- package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +6 -0
- package/es/flow/resolveViewParamsToViewList.d.ts +1 -1
- package/es/flow/utils/dataScopeRowSnapshot.d.ts +59 -0
- package/es/flow/utils/dateTimeDisplayProps.d.ts +49 -0
- package/es/flow/utils/formValueDeps.d.ts +32 -0
- package/es/flow-compat/FieldValidation.d.ts +2 -1
- package/es/index.d.ts +2 -1
- package/es/index.mjs +233 -137
- package/lib/index.js +234 -138
- package/package.json +7 -7
- package/src/PluginSettingsManager.ts +53 -0
- package/src/RouteRepository.ts +126 -24
- package/src/__tests__/PluginSettingsManager.test.ts +13 -0
- package/src/__tests__/RouteRepository.test.ts +216 -0
- package/src/__tests__/browserChecker.test.ts +61 -0
- package/src/__tests__/exports.test.ts +16 -0
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +199 -0
- package/src/__tests__/plugin-manager.test.tsx +44 -2
- package/src/__tests__/settings-center.test.tsx +40 -1
- package/src/collection-manager/field-configure.ts +1 -1
- package/src/collection-manager/interfaces/id.ts +1 -1
- package/src/collection-manager/interfaces/m2m.tsx +2 -2
- package/src/collection-manager/interfaces/m2o.tsx +2 -2
- package/src/collection-manager/interfaces/nanoid.ts +1 -1
- package/src/collection-manager/interfaces/o2m.tsx +2 -2
- package/src/collection-manager/interfaces/obo.tsx +2 -2
- package/src/collection-manager/interfaces/oho.tsx +2 -2
- package/src/collection-manager/interfaces/properties/index.ts +2 -2
- package/src/collection-manager/interfaces/uuid.ts +1 -1
- package/src/components/AppComponents.tsx +19 -3
- package/src/components/README.md +7 -1
- package/src/components/README.zh-CN.md +6 -1
- package/src/components/category-tabs/SortableCategoryTabs.tsx +210 -0
- package/src/components/category-tabs/index.ts +10 -0
- package/src/components/form/JsonTextArea.tsx +21 -11
- package/src/components/form/TypedVariableInput.tsx +416 -93
- package/src/components/form/VariableJsonTextArea.tsx +175 -0
- package/src/components/form/__tests__/JsonTextArea.test.tsx +43 -0
- package/src/components/form/__tests__/TypedVariableInput.test.tsx +268 -9
- package/src/components/form/__tests__/VariableJsonTextArea.test.tsx +86 -0
- package/src/components/form/filter/CollectionFilterItem.tsx +32 -7
- package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +38 -0
- package/src/components/form/filter/index.ts +2 -0
- package/src/components/form/index.tsx +1 -0
- package/src/components/index.ts +2 -0
- package/src/flow/__tests__/FlowRoute.test.tsx +443 -5
- package/src/flow/__tests__/getKey.test.ts +7 -0
- package/src/flow/__tests__/resolveViewParamsToViewList.test.ts +34 -0
- package/src/flow/actions/__tests__/afterSuccess.test.ts +73 -0
- package/src/flow/actions/__tests__/dataScopeFilter.test.ts +58 -0
- package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +1022 -0
- package/src/flow/actions/__tests__/fieldLinkageRules.scopeDepth.test.ts +150 -1
- package/src/flow/actions/__tests__/linkageAssignField.legacy.test.ts +135 -0
- package/src/flow/actions/__tests__/linkageRules.hiddenSync.restore.test.ts +19 -5
- package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +92 -0
- package/src/flow/actions/__tests__/openView.defineProps.route.test.tsx +104 -0
- package/src/flow/actions/__tests__/subFormFieldLinkageRules.inputArgs.test.ts +2 -2
- package/src/flow/actions/afterSuccess.tsx +142 -3
- package/src/flow/actions/customVariable.tsx +1 -2
- package/src/flow/actions/dataScopeFilter.ts +10 -0
- package/src/flow/actions/dateTimeFormat.tsx +42 -28
- package/src/flow/actions/index.ts +1 -1
- package/src/flow/actions/linkageRules.tsx +123 -35
- package/src/flow/actions/linkageRulesFormValueRefresh.ts +22 -130
- package/src/flow/actions/openView.tsx +38 -4
- package/src/flow/actions/runjs.tsx +2 -14
- package/src/flow/actions/validation.tsx +62 -30
- package/src/flow/admin-shell/BaseLayoutModel.tsx +25 -3
- package/src/flow/admin-shell/BaseLayoutRouteCoordinator.ts +5 -2
- package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +13 -0
- package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +34 -10
- package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.test.tsx +310 -0
- package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +44 -8
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.ts +5 -3
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +61 -9
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +71 -8
- package/src/flow/admin-shell/admin-layout/AdminLayoutModel.tsx +1 -1
- package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +23 -9
- package/src/flow/admin-shell/admin-layout/HelpLite.tsx +1 -3
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +188 -0
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +99 -0
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +82 -0
- package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +166 -2
- package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +20 -0
- package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +33 -5
- package/src/flow/components/DefaultValue.tsx +1 -2
- package/src/flow/components/DynamicFlowsIcon.tsx +447 -126
- package/src/flow/components/FieldAssignRulesEditor.tsx +128 -21
- package/src/flow/components/FieldAssignValueInput.tsx +10 -5
- package/src/flow/components/FlowRoute.tsx +56 -11
- package/src/flow/components/__tests__/DynamicFlowsIcon.test.tsx +148 -2
- package/src/flow/components/__tests__/FieldAssignRulesEditor.test.tsx +508 -4
- package/src/flow/components/__tests__/fieldAssignOptions.test.ts +151 -3
- package/src/flow/components/code-editor/__tests__/useCodeRunner.test.tsx +2 -17
- package/src/flow/components/code-editor/hooks/useCodeRunner.ts +2 -14
- package/src/flow/components/code-editor/runjsDiagnostics.ts +8 -45
- package/src/flow/components/fieldAssignOptions.ts +155 -27
- package/src/flow/flows/editMarkdownFlow.tsx +1 -1
- package/src/flow/getViewDiffAndUpdateHidden.tsx +1 -0
- package/src/flow/index.ts +2 -1
- package/src/flow/internal/utils/operatorSchemaHelper.ts +15 -1
- package/src/flow/models/actions/JSActionModel.tsx +2 -7
- package/src/flow/models/actions/JSCollectionActionModel.tsx +2 -7
- package/src/flow/models/actions/JSItemActionModel.tsx +2 -7
- package/src/flow/models/actions/JSRecordActionModel.tsx +2 -7
- package/src/flow/models/base/ActionModel.tsx +12 -1
- package/src/flow/models/base/ActionModelCore.tsx +6 -4
- package/src/flow/models/base/GridModel.tsx +38 -7
- package/src/flow/models/base/PageModel/PageModel.tsx +4 -1
- package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +28 -4
- package/src/flow/models/base/__tests__/ActionModel.test.ts +83 -0
- package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +37 -0
- package/src/flow/models/base/__tests__/GridModel.dragSnapshotContainer.test.ts +239 -1
- package/src/flow/models/base/__tests__/transformRowsToSingleColumn.test.ts +48 -0
- package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +31 -3
- package/src/flow/models/blocks/filter-form/FilterFormJSActionModel.tsx +2 -7
- package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +77 -0
- package/src/flow/models/blocks/filter-form/fields/FilterFormCustomFieldModel.tsx +1 -1
- package/src/flow/models/blocks/filter-manager/flow-actions/__tests__/customizeFilterRender.test.tsx +56 -1
- package/src/flow/models/blocks/form/EditFormModel.tsx +1 -0
- package/src/flow/models/blocks/form/FormActionModel.tsx +1 -1
- package/src/flow/models/blocks/form/FormBlockModel.tsx +7 -0
- package/src/flow/models/blocks/form/JSFormActionModel.tsx +2 -7
- package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +17 -0
- package/src/flow/models/blocks/form/__tests__/submitHandler.test.ts +54 -1
- package/src/flow/models/blocks/form/submitHandler.ts +17 -3
- package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +940 -75
- package/src/flow/models/blocks/form/value-runtime/rules.ts +445 -13
- package/src/flow/models/blocks/form/value-runtime/runtime.ts +348 -13
- package/src/flow/models/blocks/form/value-runtime/types.ts +2 -2
- package/src/flow/models/blocks/js-block/JSBlock.tsx +225 -9
- package/src/flow/models/blocks/js-block/__tests__/JSBlockModel.test.tsx +150 -0
- package/src/flow/models/blocks/table/JSColumnModel.tsx +1 -9
- package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +36 -13
- package/src/flow/models/blocks/table/TableBlockModel.tsx +38 -17
- package/src/flow/models/blocks/table/TableColumnModel.tsx +36 -3
- package/src/flow/models/blocks/table/__tests__/TableActionsColumnModel.test.tsx +111 -1
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.dragSort.test.ts +127 -0
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.mobileSettingsButtons.test.tsx +78 -0
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.rowSelection.test.tsx +1 -0
- package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +159 -1
- package/src/flow/models/blocks/table/dragSort/dragSortHooks.tsx +28 -17
- package/src/flow/models/blocks/table/dragSort/dragSortSettings.ts +13 -6
- package/src/flow/models/blocks/table/dragSort/dragSortUtils.ts +15 -2
- package/src/flow/models/fields/AssociationFieldModel/AssociationFieldModel.tsx +1 -1
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +29 -6
- package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +16 -4
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +141 -19
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +13 -1
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableColumnModel.rowRecord.test.ts +197 -0
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableFieldModel.reset.test.ts +180 -0
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +6 -4
- package/src/flow/models/fields/AssociationFieldModel/__tests__/AssociationFieldModel.updateAssociation.test.ts +72 -0
- package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +23 -0
- package/src/flow/models/fields/ClickableFieldModel.tsx +5 -0
- package/src/flow/models/fields/DisplayDateTimeFieldModel.tsx +29 -1
- package/src/flow/models/fields/InputFieldModel.tsx +40 -2
- package/src/flow/models/fields/JSEditableFieldModel.tsx +2 -11
- package/src/flow/models/fields/JSFieldModel.tsx +2 -7
- package/src/flow/models/fields/JSItemModel.tsx +2 -14
- package/src/flow/models/fields/SelectFieldModel.tsx +6 -4
- package/src/flow/models/fields/TextareaFieldModel.tsx +49 -3
- package/src/flow/models/fields/__tests__/DisplayDateTimeFieldModel.test.tsx +96 -0
- package/src/flow/models/fields/__tests__/InputFieldModel.test.tsx +117 -1
- package/src/flow/models/fields/__tests__/SelectFieldModel.test.tsx +43 -0
- package/src/flow/models/fields/__tests__/TextareaFieldModel.test.tsx +100 -0
- package/src/flow/models/topbar/TopbarActionModel.tsx +93 -10
- package/src/flow/resolveViewParamsToViewList.tsx +11 -3
- package/src/flow/utils/__tests__/dateTimeFormat.test.ts +258 -0
- package/src/flow/utils/dataScopeFormValueClear.ts +218 -81
- package/src/flow/utils/dataScopeRowSnapshot.ts +616 -0
- package/src/flow/utils/dateTimeDisplayProps.ts +135 -0
- package/src/flow/utils/formValueDeps.ts +170 -0
- package/src/flow-compat/FieldValidation.tsx +122 -60
- package/src/flow-compat/Popover.tsx +43 -4
- package/src/flow-compat/__tests__/Popover.test.tsx +34 -0
- package/src/index.ts +8 -1
- package/src/layout-manager/__tests__/LayoutRoute.test.tsx +41 -1
- package/src/nocobase-buildin-plugin/index.tsx +44 -14
- package/src/settings-center/AdminSettingsLayout.tsx +10 -2
- package/src/settings-center/SystemSettingsPage.tsx +1 -2
- package/src/settings-center/plugin-manager/PluginCard.tsx +2 -2
- package/src/settings-center/plugin-manager/index.tsx +3 -0
- 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
|
|
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 =
|
|
116
|
-
|
|
117
|
-
expect(
|
|
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 =
|
|
134
|
-
|
|
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
|
+
});
|