@nocobase/client-v2 2.2.0-beta.9 → 2.2.1
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,10 +7,17 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
11
|
-
|
|
10
|
+
import {
|
|
11
|
+
ACLRolesCheckProvider,
|
|
12
|
+
createMockClient,
|
|
13
|
+
NocoBaseDesktopRouteType,
|
|
14
|
+
type NocoBaseDesktopRoute,
|
|
15
|
+
Plugin,
|
|
16
|
+
} from '@nocobase/client-v2';
|
|
17
|
+
import { act, render, screen, waitFor, fireEvent } from '@testing-library/react';
|
|
12
18
|
import React from 'react';
|
|
13
19
|
import { message } from 'antd';
|
|
20
|
+
import { vi } from 'vitest';
|
|
14
21
|
import { AdminSettingsLayoutModel as ClientV2AdminSettingsLayoutModel } from '../settings-center';
|
|
15
22
|
import { AdminSettingsLayoutModel as ClientV1AdminSettingsLayoutModel } from '../../../client/src/pm/AdminSettingsLayoutModel';
|
|
16
23
|
import { NocoBaseBuildInPlugin } from '../nocobase-buildin-plugin';
|
|
@@ -46,9 +53,15 @@ const mockAdminRuntime = (
|
|
|
46
53
|
snippets?: string[];
|
|
47
54
|
pmList?: any[];
|
|
48
55
|
systemSettings?: Record<string, any>;
|
|
56
|
+
desktopRoutes?: NocoBaseDesktopRoute[];
|
|
49
57
|
} = {},
|
|
50
58
|
) => {
|
|
51
|
-
const {
|
|
59
|
+
const {
|
|
60
|
+
snippets = ['pm', 'pm.system-settings.system-settings'],
|
|
61
|
+
pmList = [],
|
|
62
|
+
systemSettings = {},
|
|
63
|
+
desktopRoutes = [],
|
|
64
|
+
} = options;
|
|
52
65
|
|
|
53
66
|
app.dataSourceManager.getCollection = ((name: string, collectionName: string) => {
|
|
54
67
|
if (name === 'main' && collectionName === 'attachments') {
|
|
@@ -92,7 +105,7 @@ const mockAdminRuntime = (
|
|
|
92
105
|
},
|
|
93
106
|
});
|
|
94
107
|
app.apiMock.onGet('/desktopRoutes:listAccessible').reply(200, {
|
|
95
|
-
data:
|
|
108
|
+
data: desktopRoutes,
|
|
96
109
|
});
|
|
97
110
|
app.apiMock.onGet('systemSettings:get').reply(200, {
|
|
98
111
|
data: {
|
|
@@ -243,6 +256,39 @@ describe('settings center', () => {
|
|
|
243
256
|
expect(await screen.findByText('Demo plugin')).toBeInTheDocument();
|
|
244
257
|
});
|
|
245
258
|
|
|
259
|
+
it('should redirect to the first accessible page when the role cannot access settings', async () => {
|
|
260
|
+
const app = createMockClient({
|
|
261
|
+
plugins: [NocoBaseBuildInPlugin, TestAclPlugin],
|
|
262
|
+
router: { type: 'memory', initialEntries: ['/admin/settings/system-settings'] },
|
|
263
|
+
});
|
|
264
|
+
mockAdminRuntime(app, {
|
|
265
|
+
snippets: ['!pm', '!pm.system-settings.system-settings'],
|
|
266
|
+
desktopRoutes: [
|
|
267
|
+
{
|
|
268
|
+
id: 1,
|
|
269
|
+
schemaUid: 'first-accessible-page',
|
|
270
|
+
title: 'First accessible page',
|
|
271
|
+
type: NocoBaseDesktopRouteType.flowPage,
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
id: 2,
|
|
275
|
+
schemaUid: 'second-accessible-page',
|
|
276
|
+
title: 'Second accessible page',
|
|
277
|
+
type: NocoBaseDesktopRouteType.flowPage,
|
|
278
|
+
},
|
|
279
|
+
],
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
await renderApp(app);
|
|
283
|
+
await waitForGetRequests(app, ['/auth:check', 'roles:check', '/desktopRoutes:listAccessible']);
|
|
284
|
+
|
|
285
|
+
await waitFor(() => {
|
|
286
|
+
expect(app.router.state.location.pathname).toBe('/admin/first-accessible-page');
|
|
287
|
+
});
|
|
288
|
+
expect(app.router.state.historyAction).toBe('REPLACE');
|
|
289
|
+
expect(screen.queryByText('Current settings page is unavailable')).not.toBeInTheDocument();
|
|
290
|
+
});
|
|
291
|
+
|
|
246
292
|
it('should hide plugin-manager menu item when pm snippet is missing', async () => {
|
|
247
293
|
const app = createMockClient({
|
|
248
294
|
plugins: [NocoBaseBuildInPlugin, TestAclPlugin],
|
|
@@ -264,12 +310,23 @@ describe('settings center', () => {
|
|
|
264
310
|
plugins: [NocoBaseBuildInPlugin, TestAclPlugin],
|
|
265
311
|
router: { type: 'memory', initialEntries: ['/admin/settings/unknown'] },
|
|
266
312
|
});
|
|
267
|
-
mockAdminRuntime(app
|
|
313
|
+
mockAdminRuntime(app, {
|
|
314
|
+
snippets: ['!pm', '!pm.system-settings.system-settings'],
|
|
315
|
+
desktopRoutes: [
|
|
316
|
+
{
|
|
317
|
+
id: 1,
|
|
318
|
+
schemaUid: 'first-accessible-page',
|
|
319
|
+
title: 'First accessible page',
|
|
320
|
+
type: NocoBaseDesktopRouteType.flowPage,
|
|
321
|
+
},
|
|
322
|
+
],
|
|
323
|
+
});
|
|
268
324
|
|
|
269
325
|
await renderApp(app);
|
|
270
326
|
await waitForGetRequests(app, ['/auth:check', 'roles:check']);
|
|
271
327
|
|
|
272
328
|
expect(await screen.findByText('Current settings page is unavailable')).toBeInTheDocument();
|
|
329
|
+
expect(app.router.state.location.pathname).toBe('/admin/settings/unknown');
|
|
273
330
|
});
|
|
274
331
|
|
|
275
332
|
it('should allow direct access to hidden page without showing menu entry', async () => {
|
|
@@ -290,13 +347,23 @@ describe('settings center', () => {
|
|
|
290
347
|
plugins: [NocoBaseBuildInPlugin, TestAclPlugin, HiddenSettingsPlugin],
|
|
291
348
|
router: { type: 'memory', initialEntries: ['/admin/settings/hidden-demo'] },
|
|
292
349
|
});
|
|
293
|
-
mockAdminRuntime(app
|
|
350
|
+
mockAdminRuntime(app, {
|
|
351
|
+
desktopRoutes: [
|
|
352
|
+
{
|
|
353
|
+
id: 1,
|
|
354
|
+
schemaUid: 'first-accessible-page',
|
|
355
|
+
title: 'First accessible page',
|
|
356
|
+
type: NocoBaseDesktopRouteType.flowPage,
|
|
357
|
+
},
|
|
358
|
+
],
|
|
359
|
+
});
|
|
294
360
|
|
|
295
361
|
await renderApp(app);
|
|
296
362
|
await waitForGetRequests(app, ['/auth:check', 'roles:check']);
|
|
297
363
|
|
|
298
364
|
expect(await screen.findByText('Hidden settings page')).toBeInTheDocument();
|
|
299
365
|
expect(screen.queryByRole('menuitem', { name: 'Hidden demo' })).not.toBeInTheDocument();
|
|
366
|
+
expect(app.router.state.location.pathname).toBe('/admin/settings/hidden-demo');
|
|
300
367
|
});
|
|
301
368
|
|
|
302
369
|
it('should show route empty state when direct access page has no permission', async () => {
|
|
@@ -319,6 +386,14 @@ describe('settings center', () => {
|
|
|
319
386
|
});
|
|
320
387
|
mockAdminRuntime(app, {
|
|
321
388
|
snippets: ['pm', 'pm.system-settings.system-settings', '!pm.secure-demo.index'],
|
|
389
|
+
desktopRoutes: [
|
|
390
|
+
{
|
|
391
|
+
id: 1,
|
|
392
|
+
schemaUid: 'first-accessible-page',
|
|
393
|
+
title: 'First accessible page',
|
|
394
|
+
type: NocoBaseDesktopRouteType.flowPage,
|
|
395
|
+
},
|
|
396
|
+
],
|
|
322
397
|
});
|
|
323
398
|
|
|
324
399
|
await renderApp(app);
|
|
@@ -326,6 +401,195 @@ describe('settings center', () => {
|
|
|
326
401
|
|
|
327
402
|
expect(await screen.findByText('Current settings page is unavailable')).toBeInTheDocument();
|
|
328
403
|
expect(screen.queryByText('Secure settings page')).not.toBeInTheDocument();
|
|
404
|
+
expect(app.router.state.location.pathname).toBe('/admin/settings/secure-demo');
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it('should redirect a denied settings tab to the first accessible tab', async () => {
|
|
408
|
+
const renderDeniedTab = vi.fn(() => <div>Denied tab content</div>);
|
|
409
|
+
|
|
410
|
+
class ProtectedSettingsTabsPlugin extends Plugin {
|
|
411
|
+
async load() {
|
|
412
|
+
this.pluginSettingsManager.addMenuItem({ key: 'protected-tabs', title: 'Protected tabs' });
|
|
413
|
+
this.pluginSettingsManager.addPageTabItem({
|
|
414
|
+
menuKey: 'protected-tabs',
|
|
415
|
+
key: 'a-second',
|
|
416
|
+
title: 'Second accessible tab',
|
|
417
|
+
aclSnippet: 'pm.protected-tabs.second',
|
|
418
|
+
sort: 20,
|
|
419
|
+
Component: () => <div>Second accessible tab content</div>,
|
|
420
|
+
});
|
|
421
|
+
this.pluginSettingsManager.addPageTabItem({
|
|
422
|
+
menuKey: 'protected-tabs',
|
|
423
|
+
key: 'z-first',
|
|
424
|
+
title: 'First accessible tab',
|
|
425
|
+
aclSnippet: 'pm.protected-tabs.first',
|
|
426
|
+
sort: 10,
|
|
427
|
+
Component: () => <div>First accessible tab content</div>,
|
|
428
|
+
});
|
|
429
|
+
this.pluginSettingsManager.addPageTabItem({
|
|
430
|
+
menuKey: 'protected-tabs',
|
|
431
|
+
key: 'denied',
|
|
432
|
+
title: 'Denied tab',
|
|
433
|
+
aclSnippet: 'pm.protected-tabs.denied',
|
|
434
|
+
sort: 30,
|
|
435
|
+
Component: renderDeniedTab,
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const app = createMockClient({
|
|
441
|
+
plugins: [NocoBaseBuildInPlugin, TestAclPlugin, ProtectedSettingsTabsPlugin],
|
|
442
|
+
router: { type: 'memory', initialEntries: ['/admin/settings/protected-tabs/denied'] },
|
|
443
|
+
});
|
|
444
|
+
mockAdminRuntime(app, {
|
|
445
|
+
snippets: ['pm', '!pm.protected-tabs.denied'],
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
await renderApp(app);
|
|
449
|
+
await waitForGetRequests(app, ['/auth:check', 'roles:check']);
|
|
450
|
+
|
|
451
|
+
await waitFor(() => {
|
|
452
|
+
expect(app.router.router.state.location.pathname).toBe('/admin/settings/protected-tabs/z-first');
|
|
453
|
+
});
|
|
454
|
+
expect(app.router.router.state.historyAction).toBe('REPLACE');
|
|
455
|
+
expect(await screen.findByText('First accessible tab content')).toBeInTheDocument();
|
|
456
|
+
expect(screen.queryByText('Denied tab content')).not.toBeInTheDocument();
|
|
457
|
+
expect(renderDeniedTab).not.toHaveBeenCalled();
|
|
458
|
+
|
|
459
|
+
await act(async () => {
|
|
460
|
+
await app.router.router.navigate('/admin/settings/protected-tabs/a-second');
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
expect(await screen.findByText('Second accessible tab content')).toBeInTheDocument();
|
|
464
|
+
expect(app.router.router.state.location.pathname).toBe('/admin/settings/protected-tabs/a-second');
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it('should skip an accessible dynamic tab when its route params cannot be resolved', async () => {
|
|
468
|
+
class DynamicSettingsTabsPlugin extends Plugin {
|
|
469
|
+
async load() {
|
|
470
|
+
this.pluginSettingsManager.addMenuItem({ key: 'dynamic-tabs', title: 'Dynamic tabs' });
|
|
471
|
+
this.pluginSettingsManager.addPageTabItem({
|
|
472
|
+
menuKey: 'dynamic-tabs',
|
|
473
|
+
key: ':name',
|
|
474
|
+
title: 'Dynamic tab',
|
|
475
|
+
aclSnippet: 'pm.dynamic-tabs.dynamic',
|
|
476
|
+
sort: 1,
|
|
477
|
+
Component: () => <div>Dynamic tab content</div>,
|
|
478
|
+
});
|
|
479
|
+
this.pluginSettingsManager.addPageTabItem({
|
|
480
|
+
menuKey: 'dynamic-tabs',
|
|
481
|
+
key: 'fallback',
|
|
482
|
+
title: 'Static fallback tab',
|
|
483
|
+
aclSnippet: 'pm.dynamic-tabs.fallback',
|
|
484
|
+
sort: 2,
|
|
485
|
+
Component: () => <div>Static fallback tab content</div>,
|
|
486
|
+
});
|
|
487
|
+
this.pluginSettingsManager.addPageTabItem({
|
|
488
|
+
menuKey: 'dynamic-tabs',
|
|
489
|
+
key: 'denied',
|
|
490
|
+
title: 'Denied tab',
|
|
491
|
+
aclSnippet: 'pm.dynamic-tabs.denied',
|
|
492
|
+
sort: 3,
|
|
493
|
+
Component: () => <div>Denied dynamic tab content</div>,
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const app = createMockClient({
|
|
499
|
+
plugins: [NocoBaseBuildInPlugin, TestAclPlugin, DynamicSettingsTabsPlugin],
|
|
500
|
+
router: { type: 'memory', initialEntries: ['/admin/settings/dynamic-tabs/denied'] },
|
|
501
|
+
});
|
|
502
|
+
mockAdminRuntime(app, {
|
|
503
|
+
snippets: ['pm', '!pm.dynamic-tabs.denied'],
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
await renderApp(app);
|
|
507
|
+
await waitForGetRequests(app, ['/auth:check', 'roles:check']);
|
|
508
|
+
|
|
509
|
+
await waitFor(() => {
|
|
510
|
+
expect(app.router.router.state.location.pathname).toBe('/admin/settings/dynamic-tabs/fallback');
|
|
511
|
+
});
|
|
512
|
+
expect(await screen.findByText('Static fallback tab content')).toBeInTheDocument();
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
it('should preserve resolved route params when redirecting between dynamic tabs', async () => {
|
|
516
|
+
class DynamicSiblingTabsPlugin extends Plugin {
|
|
517
|
+
async load() {
|
|
518
|
+
this.pluginSettingsManager.addMenuItem({ key: 'dynamic-siblings', title: 'Dynamic sibling tabs' });
|
|
519
|
+
this.pluginSettingsManager.addPageTabItem({
|
|
520
|
+
menuKey: 'dynamic-siblings',
|
|
521
|
+
key: ':name/channels',
|
|
522
|
+
title: 'Dynamic channels tab',
|
|
523
|
+
aclSnippet: 'pm.dynamic-siblings.channels',
|
|
524
|
+
sort: 1,
|
|
525
|
+
Component: () => <div>Dynamic channels tab content</div>,
|
|
526
|
+
});
|
|
527
|
+
this.pluginSettingsManager.addPageTabItem({
|
|
528
|
+
menuKey: 'dynamic-siblings',
|
|
529
|
+
key: ':name/logs',
|
|
530
|
+
title: 'Dynamic logs tab',
|
|
531
|
+
aclSnippet: 'pm.dynamic-siblings.logs',
|
|
532
|
+
sort: 2,
|
|
533
|
+
Component: () => <div>Dynamic logs tab content</div>,
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const app = createMockClient({
|
|
539
|
+
plugins: [NocoBaseBuildInPlugin, TestAclPlugin, DynamicSiblingTabsPlugin],
|
|
540
|
+
router: { type: 'memory', initialEntries: ['/admin/settings/dynamic-siblings/email:primary/logs'] },
|
|
541
|
+
});
|
|
542
|
+
mockAdminRuntime(app, {
|
|
543
|
+
snippets: ['pm', '!pm.dynamic-siblings.logs'],
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
await renderApp(app);
|
|
547
|
+
await waitForGetRequests(app, ['/auth:check', 'roles:check']);
|
|
548
|
+
|
|
549
|
+
await waitFor(() => {
|
|
550
|
+
expect(app.router.router.state.location.pathname).toBe('/admin/settings/dynamic-siblings/email:primary/channels');
|
|
551
|
+
});
|
|
552
|
+
expect(await screen.findByText('Dynamic channels tab content')).toBeInTheDocument();
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
it('should resolve hyphenated route param names when redirecting between dynamic tabs', async () => {
|
|
556
|
+
class HyphenatedParamTabsPlugin extends Plugin {
|
|
557
|
+
async load() {
|
|
558
|
+
this.pluginSettingsManager.addMenuItem({ key: 'hyphenated-params', title: 'Hyphenated param tabs' });
|
|
559
|
+
this.pluginSettingsManager.addPageTabItem({
|
|
560
|
+
menuKey: 'hyphenated-params',
|
|
561
|
+
key: ':data-source/channels',
|
|
562
|
+
title: 'Hyphenated channels tab',
|
|
563
|
+
aclSnippet: 'pm.hyphenated-params.channels',
|
|
564
|
+
sort: 1,
|
|
565
|
+
Component: () => <div>Hyphenated channels tab content</div>,
|
|
566
|
+
});
|
|
567
|
+
this.pluginSettingsManager.addPageTabItem({
|
|
568
|
+
menuKey: 'hyphenated-params',
|
|
569
|
+
key: ':data-source/logs',
|
|
570
|
+
title: 'Hyphenated logs tab',
|
|
571
|
+
aclSnippet: 'pm.hyphenated-params.logs',
|
|
572
|
+
sort: 2,
|
|
573
|
+
Component: () => <div>Hyphenated logs tab content</div>,
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const app = createMockClient({
|
|
579
|
+
plugins: [NocoBaseBuildInPlugin, TestAclPlugin, HyphenatedParamTabsPlugin],
|
|
580
|
+
router: { type: 'memory', initialEntries: ['/admin/settings/hyphenated-params/email/logs'] },
|
|
581
|
+
});
|
|
582
|
+
mockAdminRuntime(app, {
|
|
583
|
+
snippets: ['pm', '!pm.hyphenated-params.logs'],
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
await renderApp(app);
|
|
587
|
+
await waitForGetRequests(app, ['/auth:check', 'roles:check']);
|
|
588
|
+
|
|
589
|
+
await waitFor(() => {
|
|
590
|
+
expect(app.router.router.state.location.pathname).toBe('/admin/settings/hyphenated-params/email/channels');
|
|
591
|
+
});
|
|
592
|
+
expect(await screen.findByText('Hyphenated channels tab content')).toBeInTheDocument();
|
|
329
593
|
});
|
|
330
594
|
|
|
331
595
|
it('should keep menu visible when menu acl is denied but child page is visible', async () => {
|
package/src/acl/ACLProvider.tsx
CHANGED
|
@@ -11,6 +11,7 @@ import { observable } from '@formily/reactive';
|
|
|
11
11
|
import React, { useRef } from 'react';
|
|
12
12
|
import { createContext, useCallback, useContext, useEffect, useMemo, useState, type FC } from 'react';
|
|
13
13
|
import { useLocation } from 'react-router-dom';
|
|
14
|
+
import { writeFlowSettingsPreference } from '../flow/admin-shell/admin-layout/flowSettingsPreference';
|
|
14
15
|
import { useApp } from '../hooks/useApp';
|
|
15
16
|
import { createAclSnippetAllow } from './createAclSnippetAllow';
|
|
16
17
|
|
|
@@ -118,6 +119,7 @@ export const ACLRolesCheckProvider: FC = ({ children }) => {
|
|
|
118
119
|
}
|
|
119
120
|
|
|
120
121
|
if (!createAclSnippetAllow(nextData?.snippets || [], !!nextData?.allowAll)('ui.*')) {
|
|
122
|
+
writeFlowSettingsPreference(false);
|
|
121
123
|
await app.flowEngine.flowSettings.disable();
|
|
122
124
|
}
|
|
123
125
|
} catch (error) {
|
|
@@ -0,0 +1,92 @@
|
|
|
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 { render, screen, waitFor } from '@testing-library/react';
|
|
11
|
+
import React from 'react';
|
|
12
|
+
import { MemoryRouter } from 'react-router-dom';
|
|
13
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
14
|
+
import {
|
|
15
|
+
FLOW_SETTINGS_PREFERENCE_STORAGE_KEY,
|
|
16
|
+
writeFlowSettingsPreference,
|
|
17
|
+
} from '../../flow/admin-shell/admin-layout/flowSettingsPreference';
|
|
18
|
+
import { ACLRolesCheckProvider } from '../ACLProvider';
|
|
19
|
+
|
|
20
|
+
const mocks = vi.hoisted(() => ({
|
|
21
|
+
app: undefined as unknown,
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
vi.mock('../../hooks/useApp', () => ({
|
|
25
|
+
useApp: () => mocks.app,
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
describe('ACLRolesCheckProvider', () => {
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
window.localStorage.removeItem(FLOW_SETTINGS_PREFERENCE_STORAGE_KEY);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('exits UI editing mode when the current role cannot configure pages', async () => {
|
|
34
|
+
const disable = vi.fn(async () => undefined);
|
|
35
|
+
const setRole = vi.fn();
|
|
36
|
+
const aclContext: Record<string, unknown> = {};
|
|
37
|
+
const appContext = {
|
|
38
|
+
acl: undefined,
|
|
39
|
+
defineProperty(name: string, descriptor: PropertyDescriptor) {
|
|
40
|
+
Object.defineProperty(this, name, descriptor);
|
|
41
|
+
},
|
|
42
|
+
...aclContext,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
mocks.app = {
|
|
46
|
+
apiClient: {
|
|
47
|
+
auth: {
|
|
48
|
+
role: 'member',
|
|
49
|
+
setRole,
|
|
50
|
+
},
|
|
51
|
+
request: vi.fn(async () => ({
|
|
52
|
+
data: {
|
|
53
|
+
data: {
|
|
54
|
+
role: 'viewer',
|
|
55
|
+
snippets: [],
|
|
56
|
+
},
|
|
57
|
+
meta: {},
|
|
58
|
+
},
|
|
59
|
+
})),
|
|
60
|
+
},
|
|
61
|
+
context: appContext,
|
|
62
|
+
flowEngine: {
|
|
63
|
+
flowSettings: {
|
|
64
|
+
disable,
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
pluginSettingsManager: {
|
|
68
|
+
setAclSnippets: vi.fn(),
|
|
69
|
+
},
|
|
70
|
+
renderComponent: () => <div>Loading</div>,
|
|
71
|
+
router: {
|
|
72
|
+
isSkippedAuthCheckRoute: () => false,
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
writeFlowSettingsPreference(true);
|
|
76
|
+
|
|
77
|
+
render(
|
|
78
|
+
<MemoryRouter initialEntries={['/admin']}>
|
|
79
|
+
<ACLRolesCheckProvider>
|
|
80
|
+
<div>Page content</div>
|
|
81
|
+
</ACLRolesCheckProvider>
|
|
82
|
+
</MemoryRouter>,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
expect(await screen.findByText('Page content')).toBeInTheDocument();
|
|
86
|
+
await waitFor(() => {
|
|
87
|
+
expect(disable).toHaveBeenCalledTimes(1);
|
|
88
|
+
});
|
|
89
|
+
expect(window.localStorage.getItem(FLOW_SETTINGS_PREFERENCE_STORAGE_KEY)).toBe('0');
|
|
90
|
+
expect(setRole).toHaveBeenCalledWith('viewer');
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -115,6 +115,7 @@ export abstract class CollectionFieldInterface {
|
|
|
115
115
|
titleUsable?: boolean;
|
|
116
116
|
usePathOptions?(field: any): any;
|
|
117
117
|
hidden?: boolean;
|
|
118
|
+
creatable?: boolean;
|
|
118
119
|
|
|
119
120
|
addComponentOption(componentOption: CollectionFieldInterfaceComponentOption) {
|
|
120
121
|
if (!this.componentOptions) {
|
|
@@ -132,7 +132,7 @@ fieldValidationConfigureRegistry.registerMany([
|
|
|
132
132
|
{ key: 'uri', label: 'URI', hasValue: false, params: [], paramsType: 'object' },
|
|
133
133
|
{
|
|
134
134
|
key: 'multiple',
|
|
135
|
-
label: 'Multiple',
|
|
135
|
+
label: 'Multiple of',
|
|
136
136
|
hasValue: true,
|
|
137
137
|
params: [{ key: 'base', label: 'Base', componentType: 'inputNumber', required: true }],
|
|
138
138
|
},
|
package/src/components/README.md
CHANGED
|
@@ -190,6 +190,10 @@ import { TypedVariableInput } from '@nocobase/client-v2';
|
|
|
190
190
|
<Form.Item name={['options', 'secure']} label={t('Secure')} initialValue={true}>
|
|
191
191
|
<TypedVariableInput types={['boolean']} namespaces={['$env']} />
|
|
192
192
|
</Form.Item>
|
|
193
|
+
|
|
194
|
+
// Inject a custom variable tree (e.g. a workflow node's upstream outputs,
|
|
195
|
+
// which are not in the global registry)
|
|
196
|
+
<TypedVariableInput types={['string', 'number']} metaTree={workflowMetaTree} />
|
|
193
197
|
```
|
|
194
198
|
|
|
195
199
|
Key props:
|
|
@@ -197,6 +201,7 @@ Key props:
|
|
|
197
201
|
- `types`: allowed constant types. Shape mirrors v1 `useTypedConstant` — pass bare type names (`['number', 'boolean']`) or `[type, editorProps]` tuples (`[['number', { min, max, step }]]`) to forward props to the underlying antd editor. Defaults to `['string', 'number', 'boolean', 'date']`. **Even when only one type is allowed, the `Constant` entry still expands into a typed submenu** (Number / Boolean / Date / String) — matches v1 so users can see what type the constant is
|
|
198
202
|
- `namespaces`: restrict the variable picker to specific top-level namespaces (e.g. `['$env']`). Omit to expose every namespace registered on `flowEngine.context`
|
|
199
203
|
- `extraNodes`: static leaves appended after the namespace-filtered nodes
|
|
204
|
+
- `metaTree`: **inject the variable tree directly** instead of reading the global `flowEngine.context` meta tree. When set, `namespaces`/`extraNodes` are ignored and this tree is used verbatim — for context-scoped variable sources that are not in the global registry (typically a workflow node's upstream outputs, `$jobsMapByNodeKey`). Nodes whose `children` is a thunk (`() => Promise<MetaTreeNode[]>`) are **lazy-loaded on expand** (via flow-engine's `loadMetaTreeChildren`)
|
|
200
205
|
- `nullable`: whether to expose the `Null` switcher entry. Default `true`. Combined with `Form.Item.rules={[{ required: true }]}`, the user can explicitly clear the field but submission is still blocked by validation — mirrors v1's "Null + required" pairing
|
|
201
206
|
- `delimiters`: variable-token delimiters, default `['{{', '}}']` — same as `VariableInput`
|
|
202
207
|
- `value` / `onChange` / `placeholder` / `disabled` / `style` / `className`: standard controlled-input props
|
|
@@ -212,9 +217,10 @@ When **not** to use it:
|
|
|
212
217
|
- **Pure literal fields** (users will never pass a variable) → use the antd primitive directly (`InputNumber` / `Select` / `DatePicker` / `Input`) and skip the Cascader column overhead
|
|
213
218
|
- **Pure variable fields** (users will never pass a literal) → use `EnvVariableInput` (`$env`-only, with optional password masking) or `VariableInput` (general-purpose)
|
|
214
219
|
|
|
220
|
+
Supported constant types: `string` / `number` / `boolean` / `date` / `object`. The `object` (JSON) type renders an inline monospace textarea (2 rows by default, drag-resizable) — it keeps the raw text as a draft while editing and `JSON.parse`s it back into an object on blur. On a parse failure it shows the raw `JSON.parse` message (e.g. `Expected property name or '}' in JSON at position …`, matching v1) on its own row below the input, and does not emit. Mirrors v1 `useTypedConstant`'s object form (default value `{}`).
|
|
221
|
+
|
|
215
222
|
Capabilities skipped (present in v1, not yet ported to v2):
|
|
216
223
|
|
|
217
|
-
- `object` constant type (JSON editor) — v2 has no inline "JSON editor + Cascader switcher" yet; add when there's a concrete caller
|
|
218
224
|
- Async `loadChildren` cascading — most MetaTree namespaces are already eagerly resolved by `useFilteredMetaTree`, so this hasn't been needed
|
|
219
225
|
|
|
220
226
|
#### FileSizeInput
|
|
@@ -190,6 +190,9 @@ import { TypedVariableInput } from '@nocobase/client-v2';
|
|
|
190
190
|
<Form.Item name={['options', 'secure']} label={t('安全模式')} initialValue={true}>
|
|
191
191
|
<TypedVariableInput types={['boolean']} namespaces={['$env']} />
|
|
192
192
|
</Form.Item>
|
|
193
|
+
|
|
194
|
+
// 注入自定义变量树(如工作流节点的上游输出,不在全局注册表里)
|
|
195
|
+
<TypedVariableInput types={['string', 'number']} metaTree={workflowMetaTree} />
|
|
193
196
|
```
|
|
194
197
|
|
|
195
198
|
主要属性:
|
|
@@ -197,6 +200,7 @@ import { TypedVariableInput } from '@nocobase/client-v2';
|
|
|
197
200
|
- `types`:允许的常量类型。形态对齐 v1 `useTypedConstant`,可以传裸类型名 `['number', 'boolean']`,也可以传 `[type, editorProps]` 元组 `[['number', { min, max, step }]]` 把 props 透传给底层 antd 编辑器。默认 `['string', 'number', 'boolean', 'date']`。**即使只允许一种类型,「常量」入口也会展开二级菜单**(数字 / 逻辑值 / 日期 / 字符串)——跟 v1 一致,让用户能直观看到当前常量是什么类型
|
|
198
201
|
- `namespaces`:限定变量 picker 可选的顶层命名空间(如 `['$env']`)。不传就用 `flowEngine.context` 里所有已注册命名空间
|
|
199
202
|
- `extraNodes`:在命名空间过滤后追加几条静态变量节点
|
|
203
|
+
- `metaTree`:**直接注入变量树**,取代读取全局 `flowEngine.context` 的 MetaTree。传了它就**忽略** `namespaces`/`extraNodes`,原样使用这棵树——用于不在全局注册表里的、上下文相关的变量源(典型如工作流节点的上游节点输出 `$jobsMapByNodeKey`)。树里 `children` 为函数(`() => Promise<MetaTreeNode[]>`)的节点会在用户展开 Cascader 时**按需懒加载**(复用 flow-engine 的 `loadMetaTreeChildren`)
|
|
200
204
|
- `nullable`:是否暴露「空值」入口,默认 `true`。配合 `Form.Item.rules={[{ required: true }]}` 可以让用户能手动清空、但提交时会被校验拦截——跟 v1 的「空值 + required」组合一致
|
|
201
205
|
- `delimiters`:变量 token 开闭分隔符,默认 `['{{', '}}']`,跟 `VariableInput` 一致
|
|
202
206
|
- `value` / `onChange` / `placeholder` / `disabled` / `style` / `className`:标准受控字段属性
|
|
@@ -212,9 +216,10 @@ import { TypedVariableInput } from '@nocobase/client-v2';
|
|
|
212
216
|
- **纯字面量字段**(用户不会想填变量)→ 直接用 antd `InputNumber` / `Select` / `DatePicker` / `Input`,省掉 Cascader 那一格的视觉开销
|
|
213
217
|
- **纯变量字段**(用户不会想填字面量)→ 用 `EnvVariableInput`(`$env` 专用,带 password mask)或 `VariableInput`(更通用)
|
|
214
218
|
|
|
219
|
+
支持的常量类型:`string` / `number` / `boolean` / `date` / `object`。其中 `object`(即 JSON)渲染为一个等宽字体的内联 textarea(默认两行、可拖拽拉伸),编辑时保留原始文本草稿、失焦(blur)时 `JSON.parse` 回写为对象;解析失败则在输入框**下方单独一行**显示原生 `JSON.parse` 的错误信息(如 `Expected property name or '}' in JSON at position …`,对齐 v1)且不回写。对齐 v1 `useTypedConstant` 的 object 形态(默认值 `{}`)。
|
|
220
|
+
|
|
215
221
|
跳过的能力(v1 有但 v2 还没补):
|
|
216
222
|
|
|
217
|
-
- `object` 类型(JSON 编辑器)——v2 还没对应的「内联 JSON 编辑器 + Cascader 切换」组件,等真有需求再补
|
|
218
223
|
- 异步 `loadChildren` 分支——大多数命名空间的 MetaTree 已经由 `useFilteredMetaTree` 提前展平,没遇到刚需
|
|
219
224
|
|
|
220
225
|
#### FileSizeInput
|