@nocobase/client-v2 2.2.0-beta.5 → 2.2.0-beta.6
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/RouteRepository.d.ts +21 -6
- package/es/components/form/JsonTextArea.d.ts +2 -1
- package/es/components/form/VariableJsonTextArea.d.ts +19 -0
- package/es/components/form/index.d.ts +1 -0
- package/es/components/index.d.ts +1 -0
- 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 +4 -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/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/value-runtime/rules.d.ts +21 -0
- package/es/flow/models/blocks/form/value-runtime/runtime.d.ts +21 -0
- package/es/flow/models/blocks/form/value-runtime/types.d.ts +2 -2
- package/es/flow-compat/FieldValidation.d.ts +2 -1
- package/es/index.d.ts +2 -1
- package/es/index.mjs +143 -117
- package/lib/index.js +148 -122
- package/package.json +7 -7
- package/src/RouteRepository.ts +126 -24
- package/src/__tests__/RouteRepository.test.ts +216 -0
- package/src/__tests__/exports.test.ts +16 -0
- package/src/__tests__/settings-center.test.tsx +33 -0
- package/src/components/form/JsonTextArea.tsx +21 -11
- package/src/components/form/VariableJsonTextArea.tsx +175 -0
- package/src/components/form/__tests__/JsonTextArea.test.tsx +43 -0
- package/src/components/form/__tests__/VariableJsonTextArea.test.tsx +86 -0
- package/src/components/form/index.tsx +1 -0
- package/src/components/index.ts +1 -0
- package/src/flow/__tests__/FlowRoute.test.tsx +443 -5
- 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/customVariable.tsx +1 -2
- package/src/flow/actions/linkageRules.tsx +122 -34
- package/src/flow/actions/runjs.tsx +2 -14
- package/src/flow/actions/validation.tsx +62 -30
- package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +33 -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 +21 -8
- 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__/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/FieldAssignRulesEditor.tsx +62 -12
- package/src/flow/components/FlowRoute.tsx +56 -11
- package/src/flow/components/__tests__/FieldAssignRulesEditor.test.tsx +81 -4
- 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/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/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__/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/form/EditFormModel.tsx +1 -0
- 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/value-runtime/__tests__/runtime.test.ts +880 -102
- package/src/flow/models/blocks/form/value-runtime/rules.ts +445 -13
- package/src/flow/models/blocks/form/value-runtime/runtime.ts +257 -13
- package/src/flow/models/blocks/form/value-runtime/types.ts +2 -2
- package/src/flow/models/blocks/js-block/JSBlock.tsx +2 -7
- package/src/flow/models/blocks/table/JSColumnModel.tsx +1 -9
- package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +1 -1
- package/src/flow/models/blocks/table/TableBlockModel.tsx +13 -3
- package/src/flow/models/blocks/table/__tests__/TableActionsColumnModel.test.tsx +54 -0
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.mobileSettingsButtons.test.tsx +78 -0
- 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/SubTableFieldModel/SubTableField.tsx +13 -1
- 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/InputFieldModel.tsx +14 -22
- 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/__tests__/InputFieldModel.test.tsx +17 -0
- package/src/flow/models/topbar/TopbarActionModel.tsx +93 -10
- 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/settings-center/AdminSettingsLayout.tsx +3 -0
- package/src/settings-center/plugin-manager/index.tsx +3 -0
|
@@ -318,6 +318,50 @@ describe('AdminLayoutModel menu items', () => {
|
|
|
318
318
|
expect(route.children[1]._model).toBe(adminLayoutModel.subModels.menuItems?.[1]);
|
|
319
319
|
});
|
|
320
320
|
|
|
321
|
+
it('should generate ProLayout route tree under a custom admin layout route path', () => {
|
|
322
|
+
const adminLayoutModel = engine.createModel<AdminLayoutModel>({
|
|
323
|
+
uid: 'admin2-layout-model',
|
|
324
|
+
use: AdminLayoutModel,
|
|
325
|
+
props: {
|
|
326
|
+
layout: {
|
|
327
|
+
routeName: 'admin2',
|
|
328
|
+
routePath: '/admin2',
|
|
329
|
+
uid: 'admin2-layout-model',
|
|
330
|
+
layoutModelClass: 'AdminLayoutModel',
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
adminLayoutModel.syncMenuRoutes([
|
|
336
|
+
{
|
|
337
|
+
id: 1,
|
|
338
|
+
title: 'Group',
|
|
339
|
+
type: NocoBaseDesktopRouteType.group,
|
|
340
|
+
children: [
|
|
341
|
+
{
|
|
342
|
+
id: 11,
|
|
343
|
+
title: 'Page 1',
|
|
344
|
+
schemaUid: 'page-1',
|
|
345
|
+
type: NocoBaseDesktopRouteType.flowPage,
|
|
346
|
+
},
|
|
347
|
+
],
|
|
348
|
+
},
|
|
349
|
+
]);
|
|
350
|
+
|
|
351
|
+
const route = adminLayoutModel.toProLayoutRoute({
|
|
352
|
+
designable: false,
|
|
353
|
+
isMobile: false,
|
|
354
|
+
t: (title) => title,
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
expect(route.children[0].path).toBe('/admin2/1');
|
|
358
|
+
expect(route.children[0].redirect).toBe('/admin2/page-1');
|
|
359
|
+
expect(route.children[0]._runtimePath).toBe('/apps/demo/v2/admin2/page-1');
|
|
360
|
+
expect(route.children[0].routes?.[0].path).toBe('/admin2/page-1');
|
|
361
|
+
expect(route.children[0].routes?.[0].redirect).toBe('/admin2/page-1');
|
|
362
|
+
expect(route.children[0].routes?.[0]._runtimePath).toBe('/apps/demo/v2/admin2/page-1');
|
|
363
|
+
});
|
|
364
|
+
|
|
321
365
|
it('should filter legacy page menu routes but keep empty groups in v2 admin layout', () => {
|
|
322
366
|
const adminLayoutModel = engine.createModel<AdminLayoutModel>({
|
|
323
367
|
uid: 'admin-layout-model',
|
|
@@ -1128,6 +1172,46 @@ describe('AdminLayoutModel menu items', () => {
|
|
|
1128
1172
|
expect(adminLayoutModel.menuRouteRefreshVersion).toBe(refreshBefore + 1);
|
|
1129
1173
|
});
|
|
1130
1174
|
|
|
1175
|
+
it('should refresh the owning admin layout when hiding custom layout menu routes dynamically', () => {
|
|
1176
|
+
const defaultAdminLayoutModel = engine.createModel<AdminLayoutModel>({
|
|
1177
|
+
uid: ADMIN_LAYOUT_MODEL_UID,
|
|
1178
|
+
use: AdminLayoutModel,
|
|
1179
|
+
});
|
|
1180
|
+
const customAdminLayoutModel = engine.createModel<AdminLayoutModel>({
|
|
1181
|
+
uid: 'custom-admin-layout-model',
|
|
1182
|
+
use: AdminLayoutModel,
|
|
1183
|
+
props: {
|
|
1184
|
+
layout: {
|
|
1185
|
+
routeName: 'admin2',
|
|
1186
|
+
routePath: '/admin2',
|
|
1187
|
+
rootRouteName: 'admin2',
|
|
1188
|
+
uid: 'custom-admin-layout-model',
|
|
1189
|
+
layoutModelClass: 'AdminLayoutModel',
|
|
1190
|
+
rootPageModelClass: 'RootPageModel',
|
|
1191
|
+
childPageModelClass: 'ChildPageModel',
|
|
1192
|
+
authCheck: true,
|
|
1193
|
+
},
|
|
1194
|
+
},
|
|
1195
|
+
});
|
|
1196
|
+
const model = engine.createModel<AdminLayoutMenuItemModel>({
|
|
1197
|
+
uid: 'menu-item-custom-layout-dynamic-hidden',
|
|
1198
|
+
use: AdminLayoutMenuItemModel,
|
|
1199
|
+
props: {
|
|
1200
|
+
route: createRoute(),
|
|
1201
|
+
},
|
|
1202
|
+
});
|
|
1203
|
+
|
|
1204
|
+
model.setParent(customAdminLayoutModel);
|
|
1205
|
+
|
|
1206
|
+
const defaultRefreshBefore = defaultAdminLayoutModel.menuRouteRefreshVersion;
|
|
1207
|
+
const customRefreshBefore = customAdminLayoutModel.menuRouteRefreshVersion;
|
|
1208
|
+
|
|
1209
|
+
model.setHidden(true);
|
|
1210
|
+
|
|
1211
|
+
expect(customAdminLayoutModel.menuRouteRefreshVersion).toBe(customRefreshBefore + 1);
|
|
1212
|
+
expect(defaultAdminLayoutModel.menuRouteRefreshVersion).toBe(defaultRefreshBefore);
|
|
1213
|
+
});
|
|
1214
|
+
|
|
1131
1215
|
it('should render hidden menu item with opacity and keep original title in config mode', () => {
|
|
1132
1216
|
const model = engine.createModel<AdminLayoutMenuItemModel>({
|
|
1133
1217
|
uid: 'menu-item-hidden-in-config',
|
|
@@ -2222,6 +2306,21 @@ describe('AdminLayoutModel menu items', () => {
|
|
|
2222
2306
|
);
|
|
2223
2307
|
});
|
|
2224
2308
|
|
|
2309
|
+
it('should include router basename when opening same-origin links in a new window', async () => {
|
|
2310
|
+
await openAdminLayoutMenuLink({
|
|
2311
|
+
context: engine.context as any,
|
|
2312
|
+
href: '/admin2/page',
|
|
2313
|
+
params: [{ name: 'from', value: 'admin' }],
|
|
2314
|
+
openInNewWindow: true,
|
|
2315
|
+
isMobile: false,
|
|
2316
|
+
closeMobileMenu: vi.fn(),
|
|
2317
|
+
navigate: navigateMock,
|
|
2318
|
+
basenameOfCurrentRouter: '/v',
|
|
2319
|
+
});
|
|
2320
|
+
|
|
2321
|
+
expect(window.open).toHaveBeenCalledWith('/v/admin2/page?from=admin', '_blank', 'noopener,noreferrer');
|
|
2322
|
+
});
|
|
2323
|
+
|
|
2225
2324
|
it('should resolve sibling move options for non-group drag target', () => {
|
|
2226
2325
|
const activeModel = engine.createModel<AdminLayoutMenuItemModel>({
|
|
2227
2326
|
uid: 'drag-source-page',
|
|
@@ -8,12 +8,20 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import React from 'react';
|
|
11
|
-
import { render, screen } from '@testing-library/react';
|
|
11
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
|
12
|
+
import { MemoryRouter } from 'react-router-dom';
|
|
12
13
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
13
14
|
|
|
14
|
-
const { allowMock, flowModelRendererSpy } = vi.hoisted(() => {
|
|
15
|
+
const { allowMock, appMock, flowModelRendererSpy } = vi.hoisted(() => {
|
|
15
16
|
return {
|
|
16
17
|
allowMock: vi.fn(),
|
|
18
|
+
appMock: {
|
|
19
|
+
current: {
|
|
20
|
+
router: {
|
|
21
|
+
getBasename: () => '/nocobase/v',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
},
|
|
17
25
|
flowModelRendererSpy: vi.fn(),
|
|
18
26
|
};
|
|
19
27
|
});
|
|
@@ -32,6 +40,12 @@ vi.mock('../HelpLite', () => {
|
|
|
32
40
|
};
|
|
33
41
|
});
|
|
34
42
|
|
|
43
|
+
vi.mock('../../../../hooks/useApp', () => {
|
|
44
|
+
return {
|
|
45
|
+
useApp: () => appMock.current,
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
|
|
35
49
|
vi.mock('@nocobase/flow-engine', async (importOriginal) => {
|
|
36
50
|
const actual = await importOriginal<typeof import('@nocobase/flow-engine')>();
|
|
37
51
|
return {
|
|
@@ -63,10 +77,19 @@ const createAction = (options: Record<string, any>) => {
|
|
|
63
77
|
} as any;
|
|
64
78
|
};
|
|
65
79
|
|
|
80
|
+
const renderSettingsLabel = (label: React.ReactNode, pathname: string) => {
|
|
81
|
+
return render(<MemoryRouter initialEntries={[pathname]}>{label}</MemoryRouter>);
|
|
82
|
+
};
|
|
83
|
+
|
|
66
84
|
describe('TopbarActionsBar helpers', () => {
|
|
67
85
|
beforeEach(() => {
|
|
68
86
|
allowMock.mockReset();
|
|
69
87
|
flowModelRendererSpy.mockClear();
|
|
88
|
+
appMock.current = {
|
|
89
|
+
router: {
|
|
90
|
+
getBasename: () => '/nocobase/v',
|
|
91
|
+
},
|
|
92
|
+
};
|
|
70
93
|
});
|
|
71
94
|
|
|
72
95
|
afterEach(() => {
|
|
@@ -149,6 +172,147 @@ describe('TopbarActionsBar helpers', () => {
|
|
|
149
172
|
});
|
|
150
173
|
});
|
|
151
174
|
|
|
175
|
+
it('should open regular admin settings in a new tab outside admin runtime', () => {
|
|
176
|
+
const items = getTopbarPluginSettingsItems({
|
|
177
|
+
canManagePlugins: false,
|
|
178
|
+
t: (key) => key,
|
|
179
|
+
settings: [
|
|
180
|
+
{
|
|
181
|
+
key: 'system-settings',
|
|
182
|
+
name: 'system-settings',
|
|
183
|
+
title: 'System settings',
|
|
184
|
+
path: '/admin/settings/system-settings',
|
|
185
|
+
icon: null,
|
|
186
|
+
componentLoader: async () => null,
|
|
187
|
+
},
|
|
188
|
+
] as any,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
renderSettingsLabel((items as any[])[0].label, '/sales/p1');
|
|
192
|
+
|
|
193
|
+
const link = screen.getByRole('link', { name: 'System settings' });
|
|
194
|
+
expect(link).toHaveAttribute('href', '/nocobase/v/admin/settings/system-settings');
|
|
195
|
+
expect(link).toHaveAttribute('target', '_blank');
|
|
196
|
+
expect(link).toHaveAttribute('rel', expect.stringContaining('noopener'));
|
|
197
|
+
expect(link).toHaveAttribute('rel', expect.stringContaining('noreferrer'));
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('should open plugin manager in a new tab outside admin runtime', () => {
|
|
201
|
+
const items = getTopbarPluginSettingsItems({
|
|
202
|
+
canManagePlugins: true,
|
|
203
|
+
t: (key) => key,
|
|
204
|
+
settings: [
|
|
205
|
+
{
|
|
206
|
+
key: 'plugin-manager',
|
|
207
|
+
name: 'plugin-manager',
|
|
208
|
+
title: 'Plugin manager',
|
|
209
|
+
path: '/admin/settings/plugin-manager',
|
|
210
|
+
icon: null,
|
|
211
|
+
componentLoader: async () => null,
|
|
212
|
+
},
|
|
213
|
+
] as any,
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
renderSettingsLabel((items as any[])[0].label, '/sales/p1');
|
|
217
|
+
|
|
218
|
+
const link = screen.getByRole('link', { name: 'Plugin manager' });
|
|
219
|
+
expect(link).toHaveAttribute('href', '/nocobase/v/admin/settings/plugin-manager');
|
|
220
|
+
expect(link).toHaveAttribute('target', '_blank');
|
|
221
|
+
expect(link).toHaveAttribute('rel', expect.stringContaining('noopener'));
|
|
222
|
+
expect(link).toHaveAttribute('rel', expect.stringContaining('noreferrer'));
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('should keep regular admin settings as SPA links inside admin runtime', () => {
|
|
226
|
+
const items = getTopbarPluginSettingsItems({
|
|
227
|
+
canManagePlugins: false,
|
|
228
|
+
t: (key) => key,
|
|
229
|
+
settings: [
|
|
230
|
+
{
|
|
231
|
+
key: 'routes',
|
|
232
|
+
name: 'routes',
|
|
233
|
+
title: 'Routes',
|
|
234
|
+
path: '/admin/settings/routes',
|
|
235
|
+
icon: null,
|
|
236
|
+
componentLoader: async () => null,
|
|
237
|
+
},
|
|
238
|
+
] as any,
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
renderSettingsLabel((items as any[])[0].label, '/admin/settings/routes');
|
|
242
|
+
|
|
243
|
+
const link = screen.getByRole('link', { name: 'Routes' });
|
|
244
|
+
expect(link).toHaveAttribute('href', '/admin/settings/routes');
|
|
245
|
+
expect(link).not.toHaveAttribute('target', '_blank');
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('should not treat admin-like paths as admin runtime', () => {
|
|
249
|
+
const items = getTopbarPluginSettingsItems({
|
|
250
|
+
canManagePlugins: false,
|
|
251
|
+
t: (key) => key,
|
|
252
|
+
settings: [
|
|
253
|
+
{
|
|
254
|
+
key: 'system-settings',
|
|
255
|
+
name: 'system-settings',
|
|
256
|
+
title: 'System settings',
|
|
257
|
+
path: '/admin/settings/system-settings',
|
|
258
|
+
icon: null,
|
|
259
|
+
componentLoader: async () => null,
|
|
260
|
+
},
|
|
261
|
+
] as any,
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
renderSettingsLabel((items as any[])[0].label, '/admin2/foo');
|
|
265
|
+
|
|
266
|
+
const link = screen.getByRole('link', { name: 'System settings' });
|
|
267
|
+
expect(link).toHaveAttribute('href', '/nocobase/v/admin/settings/system-settings');
|
|
268
|
+
expect(link).toHaveAttribute('target', '_blank');
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('should not duplicate the basename when building new-tab settings hrefs', () => {
|
|
272
|
+
const items = getTopbarPluginSettingsItems({
|
|
273
|
+
canManagePlugins: false,
|
|
274
|
+
t: (key) => key,
|
|
275
|
+
settings: [
|
|
276
|
+
{
|
|
277
|
+
key: 'system-settings',
|
|
278
|
+
name: 'system-settings',
|
|
279
|
+
title: 'System settings',
|
|
280
|
+
path: '/nocobase/v/admin/settings/system-settings',
|
|
281
|
+
icon: null,
|
|
282
|
+
componentLoader: async () => null,
|
|
283
|
+
},
|
|
284
|
+
] as any,
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
renderSettingsLabel((items as any[])[0].label, '/sales/p1');
|
|
288
|
+
|
|
289
|
+
const link = screen.getByRole('link', { name: 'System settings' });
|
|
290
|
+
expect(link).toHaveAttribute('href', '/nocobase/v/admin/settings/system-settings');
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('should keep external settings opening in a new tab', () => {
|
|
294
|
+
const openSpy = vi.spyOn(window, 'open').mockImplementation(() => null);
|
|
295
|
+
const items = getTopbarPluginSettingsItems({
|
|
296
|
+
canManagePlugins: false,
|
|
297
|
+
t: (key) => key,
|
|
298
|
+
settings: [
|
|
299
|
+
{
|
|
300
|
+
key: 'docs',
|
|
301
|
+
name: 'docs',
|
|
302
|
+
title: 'Docs',
|
|
303
|
+
link: 'https://www.nocobase.com/docs',
|
|
304
|
+
icon: null,
|
|
305
|
+
componentLoader: async () => null,
|
|
306
|
+
},
|
|
307
|
+
] as any,
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
renderSettingsLabel((items as any[])[0].label, '/sales/p1');
|
|
311
|
+
fireEvent.click(screen.getByText('Docs'));
|
|
312
|
+
|
|
313
|
+
expect(openSpy).toHaveBeenCalledWith('https://www.nocobase.com/docs', '_blank', 'noopener,noreferrer');
|
|
314
|
+
});
|
|
315
|
+
|
|
152
316
|
it('should return empty dropdown items when plugin manager and settings are both unavailable', () => {
|
|
153
317
|
const items = getTopbarPluginSettingsItems({
|
|
154
318
|
canManagePlugins: false,
|
|
@@ -52,6 +52,26 @@ describe('resolveAdminRouteRuntimeTarget', () => {
|
|
|
52
52
|
});
|
|
53
53
|
});
|
|
54
54
|
|
|
55
|
+
it('should resolve flowPage under a custom admin layout route path', () => {
|
|
56
|
+
expect(
|
|
57
|
+
resolveAdminRouteRuntimeTarget({
|
|
58
|
+
app,
|
|
59
|
+
layout: {
|
|
60
|
+
routePath: '/admin2',
|
|
61
|
+
},
|
|
62
|
+
route: {
|
|
63
|
+
type: NocoBaseDesktopRouteType.flowPage,
|
|
64
|
+
schemaUid: 'flow-page-1',
|
|
65
|
+
},
|
|
66
|
+
}),
|
|
67
|
+
).toEqual({
|
|
68
|
+
runtimePath: '/nocobase/v2/admin2/flow-page-1',
|
|
69
|
+
navigationMode: 'spa',
|
|
70
|
+
isLegacy: false,
|
|
71
|
+
reason: 'ok',
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
55
75
|
it('should mark page unsupported in v2 admin runtime', () => {
|
|
56
76
|
expect(
|
|
57
77
|
resolveAdminRouteRuntimeTarget({
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { getModernClientPrefix, getV2EffectiveBasePath } from '../../../authRedirect';
|
|
11
11
|
import type { BaseApplication } from '../../../BaseApplication';
|
|
12
12
|
import { NocoBaseDesktopRouteType, type NocoBaseDesktopRoute } from '../../../flow-compat';
|
|
13
|
+
import type { LayoutDefinition } from '../../../layout-manager/types';
|
|
13
14
|
|
|
14
15
|
export type AdminRouteNavigationMode = 'spa' | 'document';
|
|
15
16
|
export type AdminRouteRuntimeTargetReason =
|
|
@@ -32,6 +33,8 @@ type LocationLike = {
|
|
|
32
33
|
hash?: string;
|
|
33
34
|
};
|
|
34
35
|
|
|
36
|
+
export type AdminLayoutRoutePathLike = Pick<LayoutDefinition, 'routePath'>;
|
|
37
|
+
|
|
35
38
|
type ResolveAdminRouteRuntimeTargetOptions = {
|
|
36
39
|
app: Pick<BaseApplication<any>, 'getPublicPath'> & {
|
|
37
40
|
router?: {
|
|
@@ -39,12 +42,14 @@ type ResolveAdminRouteRuntimeTargetOptions = {
|
|
|
39
42
|
};
|
|
40
43
|
};
|
|
41
44
|
route?: NocoBaseDesktopRoute;
|
|
45
|
+
layout?: AdminLayoutRoutePathLike;
|
|
42
46
|
location?: LocationLike;
|
|
43
47
|
preserveLocationState?: boolean;
|
|
44
48
|
log?: (message?: any, ...optionalParams: any[]) => void;
|
|
45
49
|
};
|
|
46
50
|
|
|
47
51
|
const LOG_PREFIX = '[NocoBase] Admin route runtime target:';
|
|
52
|
+
export const DEFAULT_ADMIN_LAYOUT_ROUTE_PATH = '/admin';
|
|
48
53
|
|
|
49
54
|
const EMPTY_TARGET: AdminRouteRuntimeTarget = {
|
|
50
55
|
runtimePath: null,
|
|
@@ -61,6 +66,25 @@ function normalizeRootRelativePath(pathname: string) {
|
|
|
61
66
|
return normalized;
|
|
62
67
|
}
|
|
63
68
|
|
|
69
|
+
export function getAdminLayoutRoutePath(layout?: AdminLayoutRoutePathLike | null) {
|
|
70
|
+
const routePath = typeof layout?.routePath === 'string' ? layout.routePath.trim() : '';
|
|
71
|
+
return routePath.startsWith('/') ? normalizeRootRelativePath(routePath) : DEFAULT_ADMIN_LAYOUT_ROUTE_PATH;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function joinAdminLayoutRoutePath(
|
|
75
|
+
layout: AdminLayoutRoutePathLike | null | undefined,
|
|
76
|
+
pathname?: string | number | null,
|
|
77
|
+
) {
|
|
78
|
+
const layoutRoutePath = getAdminLayoutRoutePath(layout);
|
|
79
|
+
const routePath = pathname == null ? '' : String(pathname).replace(/^\/+/, '');
|
|
80
|
+
|
|
81
|
+
if (!routePath) {
|
|
82
|
+
return layoutRoutePath;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return normalizeRootRelativePath(`${layoutRoutePath}/${routePath}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
64
88
|
function normalizePublicPath(value = '/') {
|
|
65
89
|
const normalized = normalizeRootRelativePath(value || '/');
|
|
66
90
|
return normalized.endsWith('/') ? normalized : `${normalized}/`;
|
|
@@ -104,8 +128,12 @@ function joinRootRelativePath(basePath: string, pathname: string) {
|
|
|
104
128
|
return normalizeRootRelativePath(`${normalizedBasePath}${normalizedPathname.slice(1)}`);
|
|
105
129
|
}
|
|
106
130
|
|
|
107
|
-
function getV2AdminPath(
|
|
108
|
-
|
|
131
|
+
function getV2AdminPath(
|
|
132
|
+
app: ResolveAdminRouteRuntimeTargetOptions['app'],
|
|
133
|
+
schemaUid: string,
|
|
134
|
+
layout?: AdminLayoutRoutePathLike,
|
|
135
|
+
) {
|
|
136
|
+
return joinRootRelativePath(getV2EffectiveBasePath(app), joinAdminLayoutRoutePath(layout, schemaUid));
|
|
109
137
|
}
|
|
110
138
|
|
|
111
139
|
function appendLocationState(pathname: string, location?: LocationLike) {
|
|
@@ -212,7 +240,7 @@ function resolvePageRuntimeTarget(
|
|
|
212
240
|
|
|
213
241
|
if (route.type === NocoBaseDesktopRouteType.flowPage) {
|
|
214
242
|
return {
|
|
215
|
-
runtimePath: getV2AdminPath(app, route.schemaUid),
|
|
243
|
+
runtimePath: getV2AdminPath(app, route.schemaUid, options.layout),
|
|
216
244
|
navigationMode: 'spa' as const,
|
|
217
245
|
isLegacy: false,
|
|
218
246
|
reason: 'ok' as const,
|
|
@@ -229,8 +257,8 @@ function resolvePageRuntimeTarget(
|
|
|
229
257
|
return {
|
|
230
258
|
runtimePath:
|
|
231
259
|
preserveLocationState && location
|
|
232
|
-
? appendLocationState(getV2AdminPath(app, route.schemaUid), location)
|
|
233
|
-
: getV2AdminPath(app, route.schemaUid),
|
|
260
|
+
? appendLocationState(getV2AdminPath(app, route.schemaUid, options.layout), location)
|
|
261
|
+
: getV2AdminPath(app, route.schemaUid, options.layout),
|
|
234
262
|
navigationMode: 'spa' as const,
|
|
235
263
|
isLegacy: false,
|
|
236
264
|
reason: 'ok' as const,
|
|
@@ -18,7 +18,6 @@ import {
|
|
|
18
18
|
parseValueToPath,
|
|
19
19
|
isRunJSValue,
|
|
20
20
|
normalizeRunJSValue,
|
|
21
|
-
runjsWithSafeGlobals,
|
|
22
21
|
useFlowContext,
|
|
23
22
|
extractPropertyPath,
|
|
24
23
|
FlowModel,
|
|
@@ -240,7 +239,7 @@ export const DefaultValue = connect((props: Props) => {
|
|
|
240
239
|
if (isRunJSValue(out)) {
|
|
241
240
|
try {
|
|
242
241
|
const { code, version } = normalizeRunJSValue(out);
|
|
243
|
-
const ret = await
|
|
242
|
+
const ret = await model?.context?.runjs(code, undefined, { version });
|
|
244
243
|
out = ret?.success ? ret.value : undefined;
|
|
245
244
|
} catch {
|
|
246
245
|
out = undefined;
|
|
@@ -28,7 +28,7 @@ import { buildFieldAssignCascaderOptionsFromCollection, type FieldAssignCascader
|
|
|
28
28
|
import { isRunJSValue, isVariableExpression, type MetaTreeNode } from '@nocobase/flow-engine';
|
|
29
29
|
import { isToManyAssociationField } from '../internal/utils/modelUtils';
|
|
30
30
|
|
|
31
|
-
export type AssignMode = 'default' | 'assign';
|
|
31
|
+
export type AssignMode = 'default' | 'assign' | 'override';
|
|
32
32
|
|
|
33
33
|
type CollectionFieldLike = {
|
|
34
34
|
name?: unknown;
|
|
@@ -89,6 +89,8 @@ export interface FieldAssignRulesEditorProps {
|
|
|
89
89
|
defaultMode?: AssignMode;
|
|
90
90
|
/** 固定 mode:用于“仅默认值/仅赋值”的场景 */
|
|
91
91
|
fixedMode?: AssignMode;
|
|
92
|
+
/** 可选 mode 列表;fixedMode 优先级更高 */
|
|
93
|
+
allowedModes?: AssignMode[];
|
|
92
94
|
/** 是否显示 condition */
|
|
93
95
|
showCondition?: boolean;
|
|
94
96
|
/** 是否显示 enable 开关 */
|
|
@@ -109,6 +111,23 @@ export interface FieldAssignRulesEditorProps {
|
|
|
109
111
|
maxAssociationFieldDepth?: number;
|
|
110
112
|
}
|
|
111
113
|
|
|
114
|
+
const ALL_ASSIGN_MODES: AssignMode[] = ['default', 'override', 'assign'];
|
|
115
|
+
|
|
116
|
+
function isAssignMode(mode: unknown): mode is AssignMode {
|
|
117
|
+
return mode === 'default' || mode === 'override' || mode === 'assign';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function normalizeAssignModes(modes?: AssignMode[]): AssignMode[] {
|
|
121
|
+
const source = Array.isArray(modes) && modes.length ? modes : ALL_ASSIGN_MODES;
|
|
122
|
+
const out: AssignMode[] = [];
|
|
123
|
+
for (const mode of source) {
|
|
124
|
+
if (!isAssignMode(mode)) continue;
|
|
125
|
+
if (out.includes(mode)) continue;
|
|
126
|
+
out.push(mode);
|
|
127
|
+
}
|
|
128
|
+
return out.length ? out : ALL_ASSIGN_MODES;
|
|
129
|
+
}
|
|
130
|
+
|
|
112
131
|
export const FieldAssignRulesEditor: React.FC<FieldAssignRulesEditorProps> = (props) => {
|
|
113
132
|
const {
|
|
114
133
|
t,
|
|
@@ -118,6 +137,7 @@ export const FieldAssignRulesEditor: React.FC<FieldAssignRulesEditorProps> = (pr
|
|
|
118
137
|
onChange,
|
|
119
138
|
defaultMode = 'assign',
|
|
120
139
|
fixedMode,
|
|
140
|
+
allowedModes: rawAllowedModes,
|
|
121
141
|
showCondition = true,
|
|
122
142
|
showEnable = true,
|
|
123
143
|
showValueEditorWhenNoField = false,
|
|
@@ -129,6 +149,28 @@ export const FieldAssignRulesEditor: React.FC<FieldAssignRulesEditorProps> = (pr
|
|
|
129
149
|
} = props;
|
|
130
150
|
|
|
131
151
|
const value = React.useMemo(() => (Array.isArray(rawValue) ? rawValue : []), [rawValue]);
|
|
152
|
+
const allowedModes = React.useMemo(
|
|
153
|
+
() => normalizeAssignModes(fixedMode ? [fixedMode] : rawAllowedModes),
|
|
154
|
+
[fixedMode, rawAllowedModes],
|
|
155
|
+
);
|
|
156
|
+
const getDefaultMode = React.useCallback((): AssignMode => {
|
|
157
|
+
if (fixedMode) return fixedMode;
|
|
158
|
+
if (isAssignMode(defaultMode) && allowedModes.includes(defaultMode)) return defaultMode;
|
|
159
|
+
return allowedModes[0] || 'assign';
|
|
160
|
+
}, [allowedModes, defaultMode, fixedMode]);
|
|
161
|
+
const normalizeItemMode = React.useCallback(
|
|
162
|
+
(item: FieldAssignRuleItem): FieldAssignRuleItem => {
|
|
163
|
+
const mode = isAssignMode(item?.mode) && allowedModes.includes(item.mode) ? item.mode : getDefaultMode();
|
|
164
|
+
return item?.mode === mode ? item : { ...item, mode };
|
|
165
|
+
},
|
|
166
|
+
[allowedModes, getDefaultMode],
|
|
167
|
+
);
|
|
168
|
+
const emitChange = React.useCallback(
|
|
169
|
+
(next: FieldAssignRuleItem[]) => {
|
|
170
|
+
onChange?.(next.map((item) => normalizeItemMode(item)));
|
|
171
|
+
},
|
|
172
|
+
[normalizeItemMode, onChange],
|
|
173
|
+
);
|
|
132
174
|
const [cascaderOptions, setCascaderOptions] = React.useState<FieldAssignCascaderOption[]>(() =>
|
|
133
175
|
Array.isArray(fieldOptions) ? (fieldOptions as FieldAssignCascaderOption[]) : [],
|
|
134
176
|
);
|
|
@@ -312,7 +354,7 @@ export const FieldAssignRulesEditor: React.FC<FieldAssignRulesEditorProps> = (pr
|
|
|
312
354
|
|
|
313
355
|
const patchItem = (index: number, patch: Partial<FieldAssignRuleItem>) => {
|
|
314
356
|
const next = value.map((it, i) => (i === index ? { ...it, ...patch } : it));
|
|
315
|
-
|
|
357
|
+
emitChange(next);
|
|
316
358
|
};
|
|
317
359
|
|
|
318
360
|
const removeItem = (index: number) => {
|
|
@@ -334,7 +376,7 @@ export const FieldAssignRulesEditor: React.FC<FieldAssignRulesEditorProps> = (pr
|
|
|
334
376
|
setSyncingRuleKey(null);
|
|
335
377
|
}
|
|
336
378
|
const next = value.filter((_, i) => i !== index);
|
|
337
|
-
|
|
379
|
+
emitChange(next);
|
|
338
380
|
};
|
|
339
381
|
|
|
340
382
|
const moveItem = (index: number, direction: 'up' | 'down') => {
|
|
@@ -343,7 +385,7 @@ export const FieldAssignRulesEditor: React.FC<FieldAssignRulesEditorProps> = (pr
|
|
|
343
385
|
if (targetIndex < 0 || targetIndex >= next.length) return;
|
|
344
386
|
const [item] = next.splice(index, 1);
|
|
345
387
|
next.splice(targetIndex, 0, item);
|
|
346
|
-
|
|
388
|
+
emitChange(next);
|
|
347
389
|
};
|
|
348
390
|
|
|
349
391
|
const addItem = () => {
|
|
@@ -352,29 +394,33 @@ export const FieldAssignRulesEditor: React.FC<FieldAssignRulesEditorProps> = (pr
|
|
|
352
394
|
{
|
|
353
395
|
key: uid(),
|
|
354
396
|
enable: true,
|
|
355
|
-
mode:
|
|
397
|
+
mode: getDefaultMode(),
|
|
356
398
|
condition: { logic: '$and', items: [] },
|
|
357
399
|
targetPath: undefined,
|
|
358
400
|
value: undefined,
|
|
359
401
|
},
|
|
360
402
|
];
|
|
361
|
-
|
|
403
|
+
emitChange(next);
|
|
362
404
|
};
|
|
363
405
|
|
|
364
406
|
const getEffectiveMode = (item: FieldAssignRuleItem): AssignMode => {
|
|
365
407
|
if (fixedMode) return fixedMode;
|
|
366
|
-
|
|
408
|
+
if (isAssignMode(item?.mode) && allowedModes.includes(item.mode)) return item.mode;
|
|
409
|
+
return getDefaultMode();
|
|
367
410
|
};
|
|
368
411
|
|
|
369
412
|
const renderAssignModeLabel = React.useCallback(
|
|
370
413
|
(mode: AssignMode) => {
|
|
371
|
-
const modeText =
|
|
414
|
+
const modeText =
|
|
415
|
+
mode === 'default' ? t('Default value') : mode === 'override' ? t('Override value') : t('Fixed value');
|
|
372
416
|
const modeHelpText =
|
|
373
417
|
mode === 'default'
|
|
374
418
|
? t(
|
|
375
419
|
'A pre-filled value. Editable, for new entries only, and won’t affect existing data (including empty values).',
|
|
376
420
|
)
|
|
377
|
-
:
|
|
421
|
+
: mode === 'override'
|
|
422
|
+
? t('Overrides existing values until the user edits it.')
|
|
423
|
+
: t('A system-set value. Read-only.');
|
|
378
424
|
|
|
379
425
|
const preventModeToggle = (e: React.MouseEvent<HTMLElement>) => {
|
|
380
426
|
e.preventDefault();
|
|
@@ -627,7 +673,8 @@ export const FieldAssignRulesEditor: React.FC<FieldAssignRulesEditorProps> = (pr
|
|
|
627
673
|
|
|
628
674
|
const renderPanelHeader = (item: FieldAssignRuleItem, index: number) => {
|
|
629
675
|
const mode = getEffectiveMode(item);
|
|
630
|
-
const modeText =
|
|
676
|
+
const modeText =
|
|
677
|
+
mode === 'default' ? t('Default value') : mode === 'override' ? t('Override value') : t('Fixed value');
|
|
631
678
|
const fieldLabel = getFieldLabel(item.targetPath);
|
|
632
679
|
const title = fieldLabel ? String(fieldLabel) : t('Please select field');
|
|
633
680
|
|
|
@@ -935,8 +982,11 @@ export const FieldAssignRulesEditor: React.FC<FieldAssignRulesEditorProps> = (pr
|
|
|
935
982
|
onChange={(event) => patchItem(index, { mode: event.target.value as AssignMode })}
|
|
936
983
|
>
|
|
937
984
|
<Space size={16}>
|
|
938
|
-
|
|
939
|
-
|
|
985
|
+
{allowedModes.map((allowedMode) => (
|
|
986
|
+
<Radio key={allowedMode} value={allowedMode}>
|
|
987
|
+
{renderAssignModeLabel(allowedMode)}
|
|
988
|
+
</Radio>
|
|
989
|
+
))}
|
|
940
990
|
</Space>
|
|
941
991
|
</Radio.Group>
|
|
942
992
|
</div>
|