@nocobase/client-v2 2.2.0-beta.9 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/es/APIClient.d.ts +13 -3
- package/es/BaseApplication.d.ts +1 -0
- package/es/collection-field-interface/CollectionFieldInterface.d.ts +1 -0
- package/es/collection-field-interface/CollectionFieldInterfaceManager.d.ts +1 -0
- package/es/components/category-tabs/SortableCategoryTabs.d.ts +55 -0
- package/es/components/category-tabs/index.d.ts +9 -0
- package/es/components/form/ScanInput/useCodeScanner.d.ts +3 -2
- package/es/components/form/TypedVariableInput.d.ts +31 -5
- package/es/components/form/filter/CollectionFilter.d.ts +2 -0
- package/es/components/form/filter/CollectionFilterItem.d.ts +11 -1
- package/es/components/form/filter/CollectionFilterPanel.d.ts +2 -0
- package/es/components/form/filter/index.d.ts +2 -0
- package/es/components/form/filter/useFilterActionProps.d.ts +2 -0
- package/es/components/index.d.ts +1 -0
- package/es/flow/actions/index.d.ts +1 -1
- package/es/flow/actions/linkageRules.d.ts +27 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +1 -0
- package/es/flow/components/FieldAssignExactDatePicker.d.ts +1 -0
- package/es/flow/components/FieldAssignValueInput.d.ts +7 -24
- package/es/flow/components/FlowRoute.d.ts +2 -2
- package/es/flow/components/RunJSValueEditor.d.ts +1 -0
- package/es/flow/components/field-value-variable/DateVariableEditor.d.ts +24 -0
- package/es/flow/components/field-value-variable/FieldValueVariableInput.d.ts +31 -0
- package/es/flow/components/field-value-variable/dateValue.d.ts +36 -0
- package/es/flow/components/field-value-variable/index.d.ts +11 -0
- package/es/flow/components/filter/FilterGroup.d.ts +1 -0
- package/es/flow/components/filter/VariableFilterItem.d.ts +6 -1
- package/es/flow/components/placeholders/BlockPlaceholder.d.ts +1 -0
- package/es/flow/index.d.ts +1 -0
- package/es/flow/internal/registerDeviceTypeContext.d.ts +10 -0
- package/es/flow/internal/utils/operatorSchemaHelper.d.ts +2 -2
- package/es/flow/models/actions/UpdateRecordActionUtils.d.ts +8 -2
- package/es/flow/models/base/ActionModelCore.d.ts +4 -2
- package/es/flow/models/base/BlockGridModel.d.ts +3 -0
- package/es/flow/models/base/CollectionBlockModel.d.ts +3 -0
- package/es/flow/models/base/PageModel/PageModel.d.ts +12 -0
- package/es/flow/models/base/PageModel/PageModelTabBar.d.ts +22 -0
- package/es/flow/models/base/PageModel/PageTabModel.d.ts +12 -0
- package/es/flow/models/blocks/details/DetailsBlockModel.d.ts +3 -0
- package/es/flow/models/blocks/filter-form/FilterFormGridModel.d.ts +1 -0
- package/es/flow/models/blocks/filter-form/FilterFormSubmitActionModel.d.ts +3 -1
- package/es/flow/models/blocks/form/FormBlockModel.d.ts +7 -0
- package/es/flow/models/blocks/form/FormGridModel.d.ts +6 -0
- package/es/flow/models/blocks/form/QuickEditFormModel.d.ts +17 -2
- package/es/flow/models/blocks/form/value-runtime/rules.d.ts +1 -0
- package/es/flow/models/blocks/table/JSColumnModel.d.ts +2 -0
- package/es/flow/models/blocks/table/TableBlockModel.d.ts +1 -0
- package/es/flow/models/blocks/table/TableColumnModel.d.ts +12 -0
- package/es/flow/models/blocks/table/utils.d.ts +1 -0
- package/es/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.d.ts +2 -1
- package/es/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.d.ts +0 -17
- package/es/flow/models/fields/AssociationFieldModel/recordSelectShared.d.ts +41 -0
- package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +15 -7
- package/es/flow-compat/fieldValidationConstants.d.ts +1 -1
- package/es/flow-compat/passwordUtils.d.ts +1 -1
- package/es/flow-compat/routeTypes.d.ts +1 -0
- package/es/index.d.ts +5 -0
- package/es/index.mjs +199 -161
- package/es/ui-operation/index.d.ts +15 -0
- package/es/ui-operation/ui-operation-codec.d.ts +10 -0
- package/es/utils/getRouteRuntimeVersion.d.ts +23 -0
- package/es/utils/index.d.ts +1 -0
- package/lib/index.js +215 -177
- package/lib/locale/languageCodes.js +3 -1
- package/package.json +9 -7
- package/src/APIClient.ts +90 -7
- package/src/Application.tsx +7 -1
- package/src/BaseApplication.tsx +11 -6
- package/src/__tests__/APIClient.test.tsx +58 -0
- package/src/__tests__/app.test.tsx +73 -0
- package/src/__tests__/browserChecker.test.ts +61 -0
- package/src/__tests__/getRouteRuntimeVersion.test.ts +68 -0
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +35 -2
- package/src/__tests__/settings-center.test.tsx +270 -6
- package/src/acl/ACLProvider.tsx +2 -0
- package/src/acl/__tests__/ACLProvider.test.tsx +92 -0
- package/src/collection-field-interface/CollectionFieldInterface.ts +1 -0
- package/src/collection-field-interface/CollectionFieldInterfaceManager.ts +1 -0
- package/src/collection-manager/field-validation.ts +1 -1
- 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 +4 -0
- package/src/components/form/ScanInput/CodeScanner.tsx +79 -35
- package/src/components/form/ScanInput/__tests__/CodeScanner.test.tsx +101 -0
- package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +284 -11
- package/src/components/form/ScanInput/useCodeScanner.ts +332 -24
- package/src/components/form/TypedVariableInput.tsx +452 -101
- package/src/components/form/__tests__/TypedVariableInput.test.tsx +364 -12
- package/src/components/form/filter/CollectionFilter.tsx +4 -0
- package/src/components/form/filter/CollectionFilterItem.tsx +36 -9
- package/src/components/form/filter/CollectionFilterPanel.tsx +4 -0
- package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +55 -0
- package/src/components/form/filter/__tests__/useFilterActionProps.test.tsx +95 -0
- package/src/components/form/filter/index.ts +2 -0
- package/src/components/form/filter/useFilterActionProps.ts +37 -6
- package/src/components/form/table/Table.tsx +81 -6
- package/src/components/form/table/__tests__/Table.columnWidth.test.tsx +433 -0
- package/src/components/index.ts +1 -0
- package/src/flow/FlowPage.tsx +9 -1
- package/src/flow/__tests__/FlowPage.test.tsx +50 -3
- package/src/flow/__tests__/FlowRoute.test.tsx +128 -10
- package/src/flow/__tests__/PluginFlowEngine.test.ts +58 -0
- package/src/flow/actions/__tests__/actionLinkageRules.forkProps.test.ts +314 -0
- package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +102 -0
- package/src/flow/actions/__tests__/linkageRules.actionStates.test.ts +33 -0
- package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +72 -0
- package/src/flow/actions/__tests__/linkageRules.tab.test.ts +171 -0
- package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +7 -3
- package/src/flow/actions/index.ts +2 -0
- package/src/flow/actions/linkageRules.tsx +144 -26
- package/src/flow/actions/linkageRulesRefresh.tsx +2 -6
- package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +0 -1
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +78 -59
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +253 -6
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +228 -0
- package/src/flow/common/Markdown/style.ts +4 -0
- package/src/flow/components/BlockItemCard.tsx +2 -2
- package/src/flow/components/FieldAssignExactDatePicker.tsx +25 -11
- package/src/flow/components/FieldAssignValueInput.tsx +72 -584
- package/src/flow/components/FlowRoute.tsx +78 -32
- package/src/flow/components/RunJSValueEditor.tsx +9 -1
- package/src/flow/components/__tests__/FieldAssignValueInput.context.test.tsx +216 -0
- package/src/flow/components/field-value-variable/DateVariableEditor.tsx +181 -0
- package/src/flow/components/field-value-variable/FieldValueVariableInput.tsx +326 -0
- package/src/flow/components/field-value-variable/__tests__/FieldValueVariableInput.test.tsx +380 -0
- package/src/flow/components/field-value-variable/dateValue.ts +223 -0
- package/src/flow/components/field-value-variable/index.ts +12 -0
- package/src/flow/components/filter/FilterGroup.tsx +33 -5
- package/src/flow/components/filter/VariableFilterItem.tsx +170 -13
- package/src/flow/components/filter/__tests__/FilterGroup.test.tsx +45 -0
- package/src/flow/components/filter/__tests__/VariableFilterItem.leftMetaTree.test.tsx +9 -0
- package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +195 -1
- package/src/flow/components/placeholders/BlockPlaceholder.tsx +70 -23
- package/src/flow/components/placeholders/__tests__/BlockPlaceholder.test.tsx +57 -7
- package/src/flow/index.ts +3 -0
- package/src/flow/internal/registerDeviceTypeContext.ts +39 -0
- package/src/flow/internal/utils/operatorSchemaHelper.ts +3 -2
- package/src/flow/models/actions/UpdateRecordActionModel.tsx +18 -1
- package/src/flow/models/actions/UpdateRecordActionUtils.ts +18 -6
- package/src/flow/models/actions/__tests__/UpdateRecordActionModel.test.ts +76 -4
- package/src/flow/models/base/ActionModelCore.tsx +11 -2
- package/src/flow/models/base/BlockGridModel.tsx +26 -0
- package/src/flow/models/base/CollectionBlockModel.tsx +38 -3
- package/src/flow/models/base/PageModel/PageModel.tsx +387 -53
- package/src/flow/models/base/PageModel/PageModelTabBar.tsx +114 -0
- package/src/flow/models/base/PageModel/PageTabModel.tsx +249 -18
- package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +790 -10
- package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.module-isolation.test.tsx +130 -0
- package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.test.tsx +118 -0
- package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +1075 -7
- package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +49 -0
- package/src/flow/models/base/__tests__/BlockGridModel.selectSceneActivation.test.ts +124 -0
- package/src/flow/models/base/__tests__/CollectionBlockModel.addAppends.test.ts +41 -0
- package/src/flow/models/base/__tests__/CollectionBlockModel.initialBeforeRenderRefresh.test.ts +125 -9
- package/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx +28 -53
- package/src/flow/models/blocks/details/DetailsBlockModel.tsx +8 -3
- package/src/flow/models/blocks/details/__tests__/DetailsBlockModel.initialPaginationChangeRefresh.test.ts +32 -1
- package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +28 -3
- package/src/flow/models/blocks/filter-form/FilterFormGridModel.tsx +36 -5
- package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +155 -25
- package/src/flow/models/blocks/filter-form/FilterFormSubmitActionModel.tsx +57 -4
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.onModelCreated.test.ts +174 -1
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +395 -1
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.getFilterValue.test.ts +72 -0
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormSubmitActionModel.lifecycle.test.ts +90 -0
- package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +66 -4
- package/src/flow/models/blocks/filter-manager/FilterManager.ts +5 -0
- package/src/flow/models/blocks/filter-manager/__tests__/FilterManager.test.ts +40 -0
- package/src/flow/models/blocks/form/FormBlockModel.tsx +47 -0
- package/src/flow/models/blocks/form/FormGridModel.tsx +4 -0
- package/src/flow/models/blocks/form/QuickEditFormModel.tsx +189 -36
- package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +8 -2
- package/src/flow/models/blocks/form/__tests__/QuickEditFormModel.quickEdit.test.ts +350 -2
- package/src/flow/models/blocks/form/__tests__/runJsFormSubmit.test.ts +131 -0
- package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +435 -0
- package/src/flow/models/blocks/form/value-runtime/rules.ts +67 -14
- package/src/flow/models/blocks/form/value-runtime/runtime.ts +43 -19
- package/src/flow/models/blocks/js-block/JSBlock.tsx +1 -1
- package/src/flow/models/blocks/table/JSColumnModel.tsx +10 -1
- package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +2 -1
- package/src/flow/models/blocks/table/TableBlockModel.tsx +29 -3
- package/src/flow/models/blocks/table/TableColumnModel.tsx +48 -9
- package/src/flow/models/blocks/table/__tests__/JSColumnModel.test.tsx +52 -5
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.filterReset.test.ts +78 -0
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.quickEditRefresh.test.ts +12 -0
- package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +33 -0
- package/src/flow/models/blocks/table/utils.ts +7 -0
- package/src/flow/models/fields/AssociationFieldModel/CascadeSelectFieldModel.tsx +33 -1
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +10 -1
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/__tests__/popupContext.test.ts +120 -0
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/actions/PopupSubTableEditActionModel.tsx +10 -2
- package/src/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.tsx +24 -1
- package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +9 -136
- package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +167 -3
- package/src/flow/models/fields/AssociationFieldModel/recordSelectShared.tsx +156 -0
- package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +149 -0
- package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +149 -113
- package/src/flow/models/fields/DisplayNumberFieldModel.tsx +1 -1
- package/src/flow/models/fields/JsonFieldModel.tsx +31 -8
- package/src/flow/models/fields/NumberFieldModel.tsx +1 -1
- package/src/flow/models/fields/VariableFieldFormModel.tsx +3 -3
- package/src/flow/models/fields/__tests__/DisplayNumberFieldModel.test.ts +33 -0
- package/src/flow/models/fields/__tests__/JsonFieldModel.test.ts +82 -0
- package/src/flow/models/fields/__tests__/NumberFieldModel.test.tsx +47 -0
- package/src/flow/models/fields/__tests__/VariableFieldFormModel.test.tsx +51 -0
- package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +26 -10
- package/src/flow/models/fields/mobile-components/MobileSelect.tsx +51 -14
- package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +139 -13
- package/src/flow/utils/dataScopeFormValueClear.ts +4 -3
- package/src/flow-compat/fieldValidationConstants.ts +1 -1
- package/src/flow-compat/routeTypes.ts +1 -0
- package/src/index.ts +5 -0
- package/src/layout-manager/LayoutContentRoute.tsx +2 -1
- package/src/layout-manager/LayoutRoute.tsx +2 -1
- package/src/layout-manager/__tests__/LayoutRoute.test.tsx +87 -1
- package/src/locale/languageCodes.ts +3 -1
- package/src/nocobase-buildin-plugin/index.tsx +12 -0
- package/src/settings-center/AdminSettingsLayout.tsx +23 -2
- package/src/ui-operation/index.ts +33 -0
- package/src/ui-operation/ui-operation-codec.ts +54 -0
- package/src/utils/getRouteRuntimeVersion.ts +185 -0
- package/src/utils/index.tsx +1 -0
|
@@ -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,110 @@ 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('keeps the number editor usable when value=null and nullable=false', async () => {
|
|
85
|
+
const ctx = createContextWithEnv();
|
|
86
|
+
const handleChange = vi.fn();
|
|
87
|
+
renderWithCtx(
|
|
88
|
+
ctx,
|
|
89
|
+
<TypedVariableInput
|
|
90
|
+
value={null}
|
|
91
|
+
types={[['number', { min: 1 }]]}
|
|
92
|
+
namespaces={['$env']}
|
|
93
|
+
nullable={false}
|
|
94
|
+
onChange={handleChange}
|
|
95
|
+
/>,
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const numberInput = await screen.findByRole('spinbutton');
|
|
99
|
+
expect(screen.queryByPlaceholderText('<Null>')).toBeNull();
|
|
100
|
+
fireEvent.change(numberInput, { target: { value: '2' } });
|
|
101
|
+
fireEvent.blur(numberInput);
|
|
102
|
+
expect(handleChange).toHaveBeenCalledWith(2);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('defaults undefined to the first constant type', async () => {
|
|
106
|
+
const ctx = createContextWithEnv();
|
|
107
|
+
const handleChange = vi.fn();
|
|
108
|
+
renderWithCtx(
|
|
109
|
+
ctx,
|
|
110
|
+
<TypedVariableInput
|
|
111
|
+
value={undefined}
|
|
112
|
+
types={['string', 'number']}
|
|
113
|
+
namespaces={['$env']}
|
|
114
|
+
nullable
|
|
115
|
+
onChange={handleChange}
|
|
116
|
+
/>,
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
expect(screen.queryByPlaceholderText('<Null>')).toBeNull();
|
|
120
|
+
expect(screen.getByRole('textbox')).toBeInTheDocument();
|
|
121
|
+
await waitFor(() => {
|
|
122
|
+
expect(handleChange).toHaveBeenCalledWith('');
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('uses a positive numeric minimum as the default value', async () => {
|
|
127
|
+
const ctx = createContextWithEnv();
|
|
128
|
+
const handleChange = vi.fn();
|
|
129
|
+
renderWithCtx(
|
|
130
|
+
ctx,
|
|
131
|
+
<TypedVariableInput
|
|
132
|
+
value={undefined}
|
|
133
|
+
types={[['number', { min: 1 }]]}
|
|
134
|
+
namespaces={['$env']}
|
|
135
|
+
nullable={false}
|
|
136
|
+
onChange={handleChange}
|
|
137
|
+
/>,
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
expect(await screen.findByDisplayValue('1')).toBeInTheDocument();
|
|
141
|
+
await waitFor(() => {
|
|
142
|
+
expect(handleChange).toHaveBeenCalledWith(1);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('can still opt out to keep the null placeholder for undefined', async () => {
|
|
147
|
+
const ctx = createContextWithEnv();
|
|
148
|
+
renderWithCtx(
|
|
149
|
+
ctx,
|
|
150
|
+
<TypedVariableInput
|
|
151
|
+
value={undefined}
|
|
152
|
+
types={['string', 'number']}
|
|
153
|
+
namespaces={['$env']}
|
|
154
|
+
nullable
|
|
155
|
+
defaultToFirstConstantTypeWhenUndefined={false}
|
|
156
|
+
onChange={() => undefined}
|
|
157
|
+
/>,
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
const nullInput = await screen.findByPlaceholderText('<Null>');
|
|
161
|
+
expect(nullInput).toBeInTheDocument();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('highlights the defaulted first constant type when the switcher opens', async () => {
|
|
165
|
+
const ctx = createContextWithEnv();
|
|
166
|
+
renderWithCtx(
|
|
167
|
+
ctx,
|
|
168
|
+
<TypedVariableInput
|
|
169
|
+
value={undefined}
|
|
170
|
+
types={['string', 'number']}
|
|
171
|
+
namespaces={['$env']}
|
|
172
|
+
nullable
|
|
173
|
+
onChange={() => undefined}
|
|
174
|
+
/>,
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
fireEvent.click(screen.getByRole('button', { name: 'variable-switcher' }));
|
|
178
|
+
|
|
179
|
+
await waitFor(() => {
|
|
180
|
+
const constant = screen.getAllByText('Constant')[0];
|
|
181
|
+
const string = screen.getAllByText('String')[0];
|
|
182
|
+
expect(constant.closest('.ant-cascader-menu-item-active')).not.toBeNull();
|
|
183
|
+
expect(string.closest('.ant-cascader-menu-item-active')).not.toBeNull();
|
|
184
|
+
});
|
|
80
185
|
});
|
|
81
186
|
});
|
|
82
187
|
|
|
@@ -99,10 +204,10 @@ describe('TypedVariableInput - variable rendering', () => {
|
|
|
99
204
|
});
|
|
100
205
|
});
|
|
101
206
|
|
|
102
|
-
it('clears back to
|
|
207
|
+
it('clears back to default-of-first-type when the close button is clicked (nullable=true)', async () => {
|
|
103
208
|
const ctx = createContextWithEnv();
|
|
104
209
|
const handleChange = vi.fn();
|
|
105
|
-
renderWithCtx(
|
|
210
|
+
const { container } = renderWithCtx(
|
|
106
211
|
ctx,
|
|
107
212
|
<TypedVariableInput
|
|
108
213
|
value="{{$env.SMTP_PORT}}"
|
|
@@ -112,27 +217,274 @@ describe('TypedVariableInput - variable rendering', () => {
|
|
|
112
217
|
onChange={handleChange}
|
|
113
218
|
/>,
|
|
114
219
|
);
|
|
115
|
-
const clear =
|
|
116
|
-
|
|
117
|
-
expect(
|
|
220
|
+
const clear = container.querySelector('button.clear-button') as HTMLButtonElement | null;
|
|
221
|
+
expect(clear).not.toBeNull();
|
|
222
|
+
expect(clear).toHaveClass('clear-button');
|
|
223
|
+
fireEvent.click(clear as HTMLButtonElement);
|
|
224
|
+
expect(handleChange).toHaveBeenCalledWith(0);
|
|
118
225
|
});
|
|
119
226
|
|
|
120
|
-
it('clears back to
|
|
227
|
+
it('clears back to the valid minimum of the first type when nullable=false', async () => {
|
|
121
228
|
const ctx = createContextWithEnv();
|
|
122
229
|
const handleChange = vi.fn();
|
|
123
|
-
renderWithCtx(
|
|
230
|
+
const { container } = renderWithCtx(
|
|
124
231
|
ctx,
|
|
125
232
|
<TypedVariableInput
|
|
126
233
|
value="{{$env.SMTP_PORT}}"
|
|
127
|
-
types={['number']}
|
|
234
|
+
types={[['number', { min: 1 }]]}
|
|
128
235
|
namespaces={['$env']}
|
|
129
236
|
nullable={false}
|
|
130
237
|
onChange={handleChange}
|
|
131
238
|
/>,
|
|
132
239
|
);
|
|
133
|
-
const clear =
|
|
134
|
-
|
|
135
|
-
|
|
240
|
+
const clear = container.querySelector('button.clear-button') as HTMLButtonElement | null;
|
|
241
|
+
expect(clear).not.toBeNull();
|
|
242
|
+
fireEvent.click(clear as HTMLButtonElement);
|
|
243
|
+
expect(handleChange).toHaveBeenCalledWith(1);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('treats types=[] as variable-only mode with a readonly placeholder before selection', async () => {
|
|
247
|
+
const ctx = createContextWithEnv();
|
|
248
|
+
renderWithCtx(
|
|
249
|
+
ctx,
|
|
250
|
+
<TypedVariableInput
|
|
251
|
+
value={undefined}
|
|
252
|
+
types={[]}
|
|
253
|
+
namespaces={['$env']}
|
|
254
|
+
placeholder="Select variable"
|
|
255
|
+
onChange={() => undefined}
|
|
256
|
+
/>,
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
const input = await screen.findByPlaceholderText('Select variable');
|
|
260
|
+
expect(input).toBeInTheDocument();
|
|
261
|
+
expect(input).toHaveAttribute('readonly');
|
|
262
|
+
expect(screen.queryByRole('button', { name: 'variable-switcher' })).toBeInTheDocument();
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('clears a variable-only selection back to null', async () => {
|
|
266
|
+
const ctx = createContextWithEnv();
|
|
267
|
+
const handleChange = vi.fn();
|
|
268
|
+
const { container } = renderWithCtx(
|
|
269
|
+
ctx,
|
|
270
|
+
<TypedVariableInput value="{{$env.SMTP_PORT}}" types={[]} namespaces={['$env']} onChange={handleChange} />,
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
const clear = container.querySelector('button.clear-button') as HTMLButtonElement | null;
|
|
274
|
+
expect(clear).not.toBeNull();
|
|
275
|
+
fireEvent.click(clear as HTMLButtonElement);
|
|
276
|
+
expect(handleChange).toHaveBeenCalledWith(null);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('hides the variable switcher when hideVariable=true in variable-only mode', async () => {
|
|
280
|
+
const ctx = createContextWithEnv();
|
|
281
|
+
renderWithCtx(
|
|
282
|
+
ctx,
|
|
283
|
+
<TypedVariableInput
|
|
284
|
+
value="{{$env.SMTP_PORT}}"
|
|
285
|
+
types={[]}
|
|
286
|
+
namespaces={['$env']}
|
|
287
|
+
hideVariable
|
|
288
|
+
onChange={() => undefined}
|
|
289
|
+
/>,
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
await waitFor(() => {
|
|
293
|
+
expect(screen.getByRole('button', { name: 'variable-tag' })).toBeInTheDocument();
|
|
294
|
+
});
|
|
295
|
+
expect(screen.queryByRole('button', { name: 'variable-switcher' })).toBeNull();
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
describe('TypedVariableInput - object / JSON constant', () => {
|
|
300
|
+
it('renders a JSON textarea for an object value when types include object', async () => {
|
|
301
|
+
const ctx = createContextWithEnv();
|
|
302
|
+
renderWithCtx(
|
|
303
|
+
ctx,
|
|
304
|
+
<TypedVariableInput value={{ a: 1 }} types={['object']} namespaces={['$env']} onChange={() => undefined} />,
|
|
305
|
+
);
|
|
306
|
+
// The object is stringified into a textarea.
|
|
307
|
+
const textarea = await screen.findByDisplayValue(/"a": 1/);
|
|
308
|
+
expect(textarea.tagName.toLowerCase()).toBe('textarea');
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('parses valid JSON on blur and emits the parsed object', async () => {
|
|
312
|
+
const ctx = createContextWithEnv();
|
|
313
|
+
const handleChange = vi.fn();
|
|
314
|
+
renderWithCtx(
|
|
315
|
+
ctx,
|
|
316
|
+
<TypedVariableInput value={{}} types={['object']} namespaces={['$env']} onChange={handleChange} />,
|
|
317
|
+
);
|
|
318
|
+
const textarea = await screen.findByDisplayValue('{}');
|
|
319
|
+
fireEvent.change(textarea, { target: { value: '{"x": 42}' } });
|
|
320
|
+
fireEvent.blur(textarea);
|
|
321
|
+
expect(handleChange).toHaveBeenLastCalledWith({ x: 42 });
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('shows an error live on change (before blur) and does not emit on invalid JSON', async () => {
|
|
325
|
+
const ctx = createContextWithEnv();
|
|
326
|
+
const handleChange = vi.fn();
|
|
327
|
+
renderWithCtx(
|
|
328
|
+
ctx,
|
|
329
|
+
<TypedVariableInput value={{}} types={['object']} namespaces={['$env']} onChange={handleChange} />,
|
|
330
|
+
);
|
|
331
|
+
const textarea = await screen.findByDisplayValue('{}');
|
|
332
|
+
// Typing an invalid value surfaces the error immediately — no blur needed (mirrors v1's `Json`, which validates on
|
|
333
|
+
// every change).
|
|
334
|
+
fireEvent.change(textarea, { target: { value: '{ not json' } });
|
|
335
|
+
await waitFor(() => {
|
|
336
|
+
// The raw `JSON.parse` error message is shown (matching v1), e.g. "Expected property name or '}' in JSON at
|
|
337
|
+
// position …".
|
|
338
|
+
expect(screen.getByText(/Expected property name/i)).toBeInTheDocument();
|
|
339
|
+
});
|
|
340
|
+
// The value is only emitted on blur, and never for an invalid value.
|
|
341
|
+
fireEvent.blur(textarea);
|
|
342
|
+
expect(handleChange).not.toHaveBeenCalled();
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it('defaults to {} when the Constant > JSON type is picked', async () => {
|
|
346
|
+
const ctx = createContextWithEnv();
|
|
347
|
+
const handleChange = vi.fn();
|
|
348
|
+
renderWithCtx(
|
|
349
|
+
ctx,
|
|
350
|
+
<TypedVariableInput value={null} types={['object']} namespaces={['$env']} onChange={handleChange} />,
|
|
351
|
+
);
|
|
352
|
+
fireEvent.click(screen.getByRole('button', { name: 'variable-switcher' }));
|
|
353
|
+
// Constant submenu → JSON leaf.
|
|
354
|
+
fireEvent.click(await screen.findByText('Constant'));
|
|
355
|
+
fireEvent.click(await screen.findByText('JSON'));
|
|
356
|
+
expect(handleChange).toHaveBeenCalledWith({});
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
describe('TypedVariableInput - injected metaTree', () => {
|
|
361
|
+
// A bare context with no global `$env` — the injected tree is the only source.
|
|
362
|
+
function createBareContext() {
|
|
363
|
+
const ctx = new FlowContext();
|
|
364
|
+
(ctx as any).t = (key: string) => key;
|
|
365
|
+
return ctx;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Mirrors a workflow node-result tree: a root whose children load lazily.
|
|
369
|
+
function makeLazyTree(loadChildren: () => Promise<MetaTreeNode[]>): MetaTreeNode[] {
|
|
370
|
+
return [
|
|
371
|
+
{
|
|
372
|
+
name: '$jobsMapByNodeKey',
|
|
373
|
+
title: 'Node result',
|
|
374
|
+
type: 'object',
|
|
375
|
+
paths: ['$jobsMapByNodeKey'],
|
|
376
|
+
children: loadChildren,
|
|
377
|
+
},
|
|
378
|
+
];
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
it('renders the injected tree in the switcher, not the global one', async () => {
|
|
382
|
+
const ctx = createBareContext();
|
|
383
|
+
const metaTree: MetaTreeNode[] = [
|
|
384
|
+
{ name: 'n1', title: 'Approval node', type: 'object', paths: ['$jobsMapByNodeKey', 'n1'] },
|
|
385
|
+
];
|
|
386
|
+
renderWithCtx(ctx, <TypedVariableInput value={null} metaTree={metaTree} onChange={() => undefined} />);
|
|
387
|
+
fireEvent.click(screen.getByRole('button', { name: 'variable-switcher' }));
|
|
388
|
+
await waitFor(() => {
|
|
389
|
+
expect(screen.getByText('Approval node')).toBeInTheDocument();
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it('lazily resolves function children when a node is expanded', async () => {
|
|
394
|
+
const ctx = createBareContext();
|
|
395
|
+
const loadChildren = vi.fn(async () => [
|
|
396
|
+
{ name: 'status', title: 'Status', type: 'string', paths: ['$jobsMapByNodeKey', 'n1', 'status'] },
|
|
397
|
+
]);
|
|
398
|
+
const metaTree = makeLazyTree(loadChildren);
|
|
399
|
+
renderWithCtx(ctx, <TypedVariableInput value={null} metaTree={metaTree} onChange={() => undefined} />);
|
|
400
|
+
|
|
401
|
+
fireEvent.click(screen.getByRole('button', { name: 'variable-switcher' }));
|
|
402
|
+
const root = await screen.findByText('Node result');
|
|
403
|
+
// Expanding the lazy node triggers loadData → loadMetaTreeChildren.
|
|
404
|
+
fireEvent.click(root);
|
|
405
|
+
await waitFor(() => {
|
|
406
|
+
expect(loadChildren).toHaveBeenCalled();
|
|
407
|
+
expect(screen.getByText('Status')).toBeInTheDocument();
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it('surfaces lazy children on the FIRST expansion (no stuck-loading regression)', async () => {
|
|
412
|
+
const ctx = createBareContext();
|
|
413
|
+
// A deferred promise so loading→resolved is a real two-phase transition, reproducing the rc-cascader
|
|
414
|
+
// reference-cache bug where the first expand stayed stuck on the spinner until the column was reopened.
|
|
415
|
+
let resolveChildren: (nodes: MetaTreeNode[]) => void = () => undefined;
|
|
416
|
+
const childrenPromise = new Promise<MetaTreeNode[]>((resolve) => {
|
|
417
|
+
resolveChildren = resolve;
|
|
418
|
+
});
|
|
419
|
+
const loadChildren = vi.fn(() => childrenPromise);
|
|
420
|
+
const metaTree = makeLazyTree(loadChildren);
|
|
421
|
+
renderWithCtx(ctx, <TypedVariableInput value={null} metaTree={metaTree} onChange={() => undefined} />);
|
|
422
|
+
|
|
423
|
+
fireEvent.click(screen.getByRole('button', { name: 'variable-switcher' }));
|
|
424
|
+
const root = await screen.findByText('Node result');
|
|
425
|
+
fireEvent.click(root);
|
|
426
|
+
expect(loadChildren).toHaveBeenCalledTimes(1);
|
|
427
|
+
// Resolve after the click — the child must appear without reopening.
|
|
428
|
+
resolveChildren([
|
|
429
|
+
{ name: 'secret', title: 'SMTP_HOST', type: 'string', paths: ['$jobsMapByNodeKey', 'n1', 'secret'] },
|
|
430
|
+
]);
|
|
431
|
+
await waitFor(() => {
|
|
432
|
+
expect(screen.getByText('SMTP_HOST')).toBeInTheDocument();
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it('emits the selected variable as a {{ ... }} expression with no inner spaces', async () => {
|
|
437
|
+
const ctx = createBareContext();
|
|
438
|
+
const handleChange = vi.fn();
|
|
439
|
+
const metaTree: MetaTreeNode[] = [
|
|
440
|
+
{ name: 'n1', title: 'Approval node', type: 'string', paths: ['$jobsMapByNodeKey', 'n1'] },
|
|
441
|
+
];
|
|
442
|
+
renderWithCtx(ctx, <TypedVariableInput value={null} metaTree={metaTree} onChange={handleChange} />);
|
|
443
|
+
fireEvent.click(screen.getByRole('button', { name: 'variable-switcher' }));
|
|
444
|
+
const leaf = await screen.findByText('Approval node');
|
|
445
|
+
fireEvent.click(leaf);
|
|
446
|
+
await waitFor(() => {
|
|
447
|
+
expect(handleChange).toHaveBeenCalledWith('{{$jobsMapByNodeKey.n1}}');
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
it('preloads a saved variable label across a lazy level (deep label not dropped on mount)', async () => {
|
|
452
|
+
const ctx = createBareContext();
|
|
453
|
+
// The query-node case: a saved reference `$jobsMapByNodeKey.n1.role` points below `n1`, whose children are a lazy
|
|
454
|
+
// thunk (relation not yet expanded). On mount the tag must show the full path "Query / Role", not stop at "Query" —
|
|
455
|
+
// the component walks the saved path resolving each lazy level. (Regression: reopening a saved condition showed
|
|
456
|
+
// only "节点数据 / 查询数据" and dropped "角色标识".)
|
|
457
|
+
const loadRoleFields = vi.fn(async () => [
|
|
458
|
+
{ name: 'role', title: 'Role', type: 'string', paths: ['$jobsMapByNodeKey', 'n1', 'role'] },
|
|
459
|
+
]);
|
|
460
|
+
const metaTree: MetaTreeNode[] = [
|
|
461
|
+
{
|
|
462
|
+
name: '$jobsMapByNodeKey',
|
|
463
|
+
title: 'Node result',
|
|
464
|
+
type: 'object',
|
|
465
|
+
paths: ['$jobsMapByNodeKey'],
|
|
466
|
+
children: [
|
|
467
|
+
{
|
|
468
|
+
name: 'n1',
|
|
469
|
+
title: 'Query',
|
|
470
|
+
type: 'object',
|
|
471
|
+
paths: ['$jobsMapByNodeKey', 'n1'],
|
|
472
|
+
children: loadRoleFields,
|
|
473
|
+
},
|
|
474
|
+
],
|
|
475
|
+
},
|
|
476
|
+
];
|
|
477
|
+
renderWithCtx(
|
|
478
|
+
ctx,
|
|
479
|
+
<TypedVariableInput value="{{$jobsMapByNodeKey.n1.role}}" metaTree={metaTree} onChange={() => undefined} />,
|
|
480
|
+
);
|
|
481
|
+
const tag = screen.getByRole('button', { name: 'variable-tag' });
|
|
482
|
+
// Preload resolves the lazy level so the deep label appears without expanding.
|
|
483
|
+
await waitFor(() => {
|
|
484
|
+
expect(loadRoleFields).toHaveBeenCalled();
|
|
485
|
+
expect(tag.textContent).toContain('Role');
|
|
486
|
+
});
|
|
487
|
+
expect(tag.textContent).toContain('Query');
|
|
136
488
|
});
|
|
137
489
|
});
|
|
138
490
|
|
|
@@ -21,6 +21,8 @@ export interface CollectionFilterProps {
|
|
|
21
21
|
collection: Collection | undefined;
|
|
22
22
|
/** Previously compiled filter param used to seed the editable filter group. */
|
|
23
23
|
initialValue?: CompiledFilter;
|
|
24
|
+
/** Default compiled filter used both for initial empty state and Reset. */
|
|
25
|
+
defaultValue?: CompiledFilter;
|
|
24
26
|
/** Called on Submit or Reset with the compiled NocoBase filter param (`undefined` when cleared). */
|
|
25
27
|
onChange: (filter: CompiledFilter) => void;
|
|
26
28
|
/** Translator. Defaults to identity. */
|
|
@@ -58,6 +60,7 @@ export const CollectionFilter: FC<CollectionFilterProps> = (props) => {
|
|
|
58
60
|
const {
|
|
59
61
|
collection,
|
|
60
62
|
initialValue,
|
|
63
|
+
defaultValue,
|
|
61
64
|
onChange,
|
|
62
65
|
t = identity,
|
|
63
66
|
filterableFieldNames,
|
|
@@ -75,6 +78,7 @@ export const CollectionFilter: FC<CollectionFilterProps> = (props) => {
|
|
|
75
78
|
const filterAction = useFilterActionProps({
|
|
76
79
|
collection,
|
|
77
80
|
initialValue,
|
|
81
|
+
defaultValue,
|
|
78
82
|
filterableFieldNames,
|
|
79
83
|
nonfilterableFieldNames,
|
|
80
84
|
noIgnore,
|
|
@@ -58,6 +58,16 @@ export interface CollectionFilterItemProps {
|
|
|
58
58
|
t?: (key: string) => string;
|
|
59
59
|
/** Optional v2 app registry used to resolve plugin-provided operator components. */
|
|
60
60
|
app?: { getComponent?: (name: string) => React.ComponentType<any> | undefined };
|
|
61
|
+
/** Optional override for the left field picker placeholder. */
|
|
62
|
+
fieldPlaceholder?: string;
|
|
63
|
+
/** Optional override for the operator picker placeholder. */
|
|
64
|
+
operatorPlaceholder?: string;
|
|
65
|
+
/** Optional override for the value input placeholder. Pass `null` to suppress it. */
|
|
66
|
+
valuePlaceholder?: string | null;
|
|
67
|
+
/** Optional override for the left field picker width. Defaults to v2's original 200px. */
|
|
68
|
+
fieldWidth?: number;
|
|
69
|
+
/** Optional override for the operator picker min-width. Defaults to v2's original 120px. */
|
|
70
|
+
operatorMinWidth?: number;
|
|
61
71
|
}
|
|
62
72
|
|
|
63
73
|
const identity = (s: string) => s;
|
|
@@ -98,6 +108,12 @@ export const CollectionFilterItem: FC<CollectionFilterItemProps> = observer(
|
|
|
98
108
|
const { path: leftValue, operator, value: rightValue } = props.value;
|
|
99
109
|
const flowEngine = useFlowEngine({ throwError: false }) as any;
|
|
100
110
|
const app = props.app || flowEngine?.context?.app;
|
|
111
|
+
const fieldPlaceholder = props.fieldPlaceholder ?? t('Select field');
|
|
112
|
+
const operatorPlaceholder = props.operatorPlaceholder ?? t('Comparision');
|
|
113
|
+
const valuePlaceholder =
|
|
114
|
+
props.valuePlaceholder === undefined ? t('Enter value') : props.valuePlaceholder || undefined;
|
|
115
|
+
const fieldWidth = props.fieldWidth ?? 200;
|
|
116
|
+
const operatorMinWidth = props.operatorMinWidth ?? 120;
|
|
101
117
|
|
|
102
118
|
const options = useFilterOptions(collection, { filterableFieldNames, nonfilterableFieldNames, noIgnore, t });
|
|
103
119
|
|
|
@@ -114,8 +130,10 @@ export const CollectionFilterItem: FC<CollectionFilterItemProps> = observer(
|
|
|
114
130
|
[operatorOptions, operator],
|
|
115
131
|
);
|
|
116
132
|
|
|
117
|
-
const handleFieldChange = (next
|
|
118
|
-
|
|
133
|
+
const handleFieldChange = (next?: (string | number)[]) => {
|
|
134
|
+
// rc-cascader emits `undefined` when its clear icon is clicked in single-select mode, despite its public type
|
|
135
|
+
// declaring an array. Treat that runtime value as an empty path so clearing the field also resets the row.
|
|
136
|
+
const path = (next ?? []).map(String);
|
|
119
137
|
props.value.path = path.join('.');
|
|
120
138
|
const leaf = findOptionByPath(options, path);
|
|
121
139
|
props.value.operator = leaf?.operators?.[0]?.value || '';
|
|
@@ -135,8 +153,8 @@ export const CollectionFilterItem: FC<CollectionFilterItemProps> = observer(
|
|
|
135
153
|
return (
|
|
136
154
|
<Space wrap>
|
|
137
155
|
<Cascader
|
|
138
|
-
style={{ width:
|
|
139
|
-
placeholder={
|
|
156
|
+
style={{ width: fieldWidth }}
|
|
157
|
+
placeholder={fieldPlaceholder}
|
|
140
158
|
options={cascaderOptions}
|
|
141
159
|
value={fieldPath}
|
|
142
160
|
onChange={handleFieldChange}
|
|
@@ -145,15 +163,15 @@ export const CollectionFilterItem: FC<CollectionFilterItemProps> = observer(
|
|
|
145
163
|
popupClassName={cascaderPopupClass}
|
|
146
164
|
/>
|
|
147
165
|
<Select
|
|
148
|
-
style={{
|
|
149
|
-
placeholder={
|
|
166
|
+
style={{ minWidth: operatorMinWidth }}
|
|
167
|
+
placeholder={operatorPlaceholder}
|
|
150
168
|
value={operator || undefined}
|
|
151
169
|
onChange={handleOperatorChange}
|
|
152
170
|
disabled={!leftValue || operatorOptions.length === 0}
|
|
153
171
|
>
|
|
154
172
|
{operatorOptions.map((op) => (
|
|
155
173
|
<Select.Option key={op.value} value={op.value}>
|
|
156
|
-
{op.label}
|
|
174
|
+
{typeof op.label === 'string' ? t(op.label) : op.label}
|
|
157
175
|
</Select.Option>
|
|
158
176
|
))}
|
|
159
177
|
</Select>
|
|
@@ -162,7 +180,7 @@ export const CollectionFilterItem: FC<CollectionFilterItemProps> = observer(
|
|
|
162
180
|
operator={selectedOperator}
|
|
163
181
|
value={rightValue}
|
|
164
182
|
onChange={handleValueChange}
|
|
165
|
-
placeholder={
|
|
183
|
+
placeholder={valuePlaceholder}
|
|
166
184
|
t={t}
|
|
167
185
|
app={app}
|
|
168
186
|
/>
|
|
@@ -179,7 +197,16 @@ export function createCollectionFilterItem(
|
|
|
179
197
|
collection: Collection,
|
|
180
198
|
bound?: Pick<
|
|
181
199
|
CollectionFilterItemProps,
|
|
182
|
-
|
|
200
|
+
| 'filterableFieldNames'
|
|
201
|
+
| 'nonfilterableFieldNames'
|
|
202
|
+
| 'noIgnore'
|
|
203
|
+
| 't'
|
|
204
|
+
| 'app'
|
|
205
|
+
| 'fieldPlaceholder'
|
|
206
|
+
| 'operatorPlaceholder'
|
|
207
|
+
| 'valuePlaceholder'
|
|
208
|
+
| 'fieldWidth'
|
|
209
|
+
| 'operatorMinWidth'
|
|
183
210
|
>,
|
|
184
211
|
) {
|
|
185
212
|
const Component: FC<{ value: CollectionFilterItemValue }> = (props) => (
|
|
@@ -30,6 +30,8 @@ export interface CollectionFilterPanelProps {
|
|
|
30
30
|
collection: Collection | undefined;
|
|
31
31
|
/** Previously compiled filter param used to seed the editable filter group. */
|
|
32
32
|
initialValue?: CompiledFilter;
|
|
33
|
+
/** Default compiled filter used both for initial empty state and Reset. */
|
|
34
|
+
defaultValue?: CompiledFilter;
|
|
33
35
|
/** Called when the condition group structure changes or `reset()` is invoked. */
|
|
34
36
|
onChange?: (filter: CompiledFilter) => void;
|
|
35
37
|
/** Translator. Defaults to identity. */
|
|
@@ -51,6 +53,7 @@ export const CollectionFilterPanel = forwardRef<CollectionFilterPanelRef, Collec
|
|
|
51
53
|
const {
|
|
52
54
|
collection,
|
|
53
55
|
initialValue,
|
|
56
|
+
defaultValue,
|
|
54
57
|
onChange,
|
|
55
58
|
t = identity,
|
|
56
59
|
filterableFieldNames,
|
|
@@ -61,6 +64,7 @@ export const CollectionFilterPanel = forwardRef<CollectionFilterPanelRef, Collec
|
|
|
61
64
|
const filterAction = useFilterActionProps({
|
|
62
65
|
collection,
|
|
63
66
|
initialValue,
|
|
67
|
+
defaultValue,
|
|
64
68
|
filterableFieldNames,
|
|
65
69
|
nonfilterableFieldNames,
|
|
66
70
|
noIgnore,
|
|
@@ -104,6 +104,23 @@ describe('CollectionFilterItem', () => {
|
|
|
104
104
|
});
|
|
105
105
|
});
|
|
106
106
|
|
|
107
|
+
it('clears the filter row when the selected field is cleared', async () => {
|
|
108
|
+
const value = observable({ path: 'username', operator: '$eq', value: 'alice' });
|
|
109
|
+
const collection = buildStubCollection([{ name: 'username', title: 'Username' }]);
|
|
110
|
+
|
|
111
|
+
const { container } = render(<CollectionFilterItem value={value} collection={collection} />);
|
|
112
|
+
|
|
113
|
+
const clear = container.querySelector('.ant-select-clear');
|
|
114
|
+
if (!clear) throw new Error('expected Cascader clear control to be rendered');
|
|
115
|
+
fireEvent.mouseDown(clear);
|
|
116
|
+
|
|
117
|
+
await waitFor(() => {
|
|
118
|
+
expect(value.path).toBe('');
|
|
119
|
+
expect(value.operator).toBe('');
|
|
120
|
+
expect(value.value).toBeUndefined();
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
107
124
|
it('clears value when the operator changes', async () => {
|
|
108
125
|
const value = observable({ path: 'username', operator: '$eq', value: 'alice' });
|
|
109
126
|
const collection = buildStubCollection([{ name: 'username', title: 'Username' }]);
|
|
@@ -244,4 +261,42 @@ describe('CollectionFilterItem', () => {
|
|
|
244
261
|
expect(value.path).toBe('username');
|
|
245
262
|
});
|
|
246
263
|
});
|
|
264
|
+
|
|
265
|
+
it('keeps default widths at 200/120 and honours explicit width/placeholder overrides', () => {
|
|
266
|
+
const value = observable({ path: 'username', operator: '$eq', value: '' });
|
|
267
|
+
const collection = buildStubCollection([{ name: 'username', title: 'Username' }]);
|
|
268
|
+
|
|
269
|
+
const { container, rerender } = render(<CollectionFilterItem value={value} collection={collection} />);
|
|
270
|
+
|
|
271
|
+
let selects = container.querySelectorAll('.ant-select');
|
|
272
|
+
expect(selects[0]).toHaveStyle({ width: '200px' });
|
|
273
|
+
expect(selects[1]).toHaveStyle({ minWidth: '120px' });
|
|
274
|
+
|
|
275
|
+
rerender(
|
|
276
|
+
<CollectionFilterItem
|
|
277
|
+
value={observable({ path: '', operator: '', value: '' })}
|
|
278
|
+
collection={collection}
|
|
279
|
+
fieldWidth={160}
|
|
280
|
+
operatorMinWidth={110}
|
|
281
|
+
fieldPlaceholder="Choose field"
|
|
282
|
+
operatorPlaceholder="Choose operator"
|
|
283
|
+
/>,
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
selects = container.querySelectorAll('.ant-select');
|
|
287
|
+
expect(selects[0]).toHaveStyle({ width: '160px' });
|
|
288
|
+
expect(selects[1]).toHaveStyle({ minWidth: '110px' });
|
|
289
|
+
expect(screen.getByText('Choose field')).toBeInTheDocument();
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('lets callers suppress the fallback value placeholder', () => {
|
|
293
|
+
const value = observable({ path: 'username', operator: '$eq', value: '' });
|
|
294
|
+
const collection = buildStubCollection([{ name: 'username', title: 'Username' }]);
|
|
295
|
+
|
|
296
|
+
const { rerender } = render(<CollectionFilterItem value={value} collection={collection} />);
|
|
297
|
+
expect(screen.getByPlaceholderText('Enter value')).toBeInTheDocument();
|
|
298
|
+
|
|
299
|
+
rerender(<CollectionFilterItem value={value} collection={collection} valuePlaceholder={null} />);
|
|
300
|
+
expect(screen.queryByPlaceholderText('Enter value')).not.toBeInTheDocument();
|
|
301
|
+
});
|
|
247
302
|
});
|