@nocobase/client-v2 3.0.0-alpha.7 → 3.0.0-alpha.9
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/flow/actions/linkageRules.d.ts +24 -0
- package/es/flow/components/FieldAssignValueInput.d.ts +4 -30
- 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/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/value-runtime/rules.d.ts +1 -0
- package/es/index.mjs +125 -134
- package/lib/index.js +119 -128
- package/package.json +7 -7
- package/src/__tests__/app.test.tsx +15 -6
- package/src/__tests__/settings-center.test.tsx +222 -21
- package/src/components/AppComponents.tsx +4 -4
- package/src/flow/actions/__tests__/linkageRules.actionStates.test.ts +33 -0
- package/src/flow/actions/linkageRules.tsx +25 -7
- package/src/flow/components/FieldAssignValueInput.tsx +38 -621
- 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/models/blocks/assign-form/AssignFormItemModel.tsx +28 -53
- 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/__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/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +8 -0
- 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/DisplayNumberFieldModel.tsx +1 -1
- package/src/flow/models/fields/NumberFieldModel.tsx +1 -1
- package/src/flow/models/fields/__tests__/DisplayNumberFieldModel.test.ts +33 -0
- package/src/flow/models/fields/__tests__/NumberFieldModel.test.tsx +47 -0
- package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +1 -0
- package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +20 -2
- package/src/settings-center/AdminSettingsLayout.tsx +38 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/client-v2",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.9",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"module": "es/index.mjs",
|
|
@@ -27,11 +27,11 @@
|
|
|
27
27
|
"@formily/antd-v5": "1.2.3",
|
|
28
28
|
"@formily/react": "^2.2.27",
|
|
29
29
|
"@formily/shared": "^2.2.27",
|
|
30
|
-
"@nocobase/evaluators": "3.0.0-alpha.
|
|
31
|
-
"@nocobase/flow-engine": "3.0.0-alpha.
|
|
32
|
-
"@nocobase/sdk": "3.0.0-alpha.
|
|
33
|
-
"@nocobase/shared": "3.0.0-alpha.
|
|
34
|
-
"@nocobase/utils": "3.0.0-alpha.
|
|
30
|
+
"@nocobase/evaluators": "3.0.0-alpha.9",
|
|
31
|
+
"@nocobase/flow-engine": "3.0.0-alpha.9",
|
|
32
|
+
"@nocobase/sdk": "3.0.0-alpha.9",
|
|
33
|
+
"@nocobase/shared": "3.0.0-alpha.9",
|
|
34
|
+
"@nocobase/utils": "3.0.0-alpha.9",
|
|
35
35
|
"ahooks": "^3.7.2",
|
|
36
36
|
"antd": "5.24.2",
|
|
37
37
|
"antd-style": "3.7.1",
|
|
@@ -48,5 +48,5 @@
|
|
|
48
48
|
"react-i18next": "^11.15.1",
|
|
49
49
|
"react-router-dom": "^6.30.1"
|
|
50
50
|
},
|
|
51
|
-
"gitHead": "
|
|
51
|
+
"gitHead": "82aaad40c5be1120839a11301a48e7fe690f48c3"
|
|
52
52
|
}
|
|
@@ -379,7 +379,11 @@ describe('app', () => {
|
|
|
379
379
|
expect(screen.getByText('maintaining dialog message')).toBeInTheDocument();
|
|
380
380
|
});
|
|
381
381
|
|
|
382
|
-
it(
|
|
382
|
+
it.each([
|
|
383
|
+
['pm.enable', 'Enabling plugin'],
|
|
384
|
+
['pm.disable', 'Disabling plugin'],
|
|
385
|
+
['future.command', 'future.command'],
|
|
386
|
+
])('should keep current content behind the default dialog while app commanding: %s', async (commandName, title) => {
|
|
383
387
|
const CurrentPage = () => {
|
|
384
388
|
const [count, setCount] = React.useState(0);
|
|
385
389
|
return <button onClick={() => setCount((value) => value + 1)}>Current page count: {count}</button>;
|
|
@@ -398,12 +402,12 @@ describe('app', () => {
|
|
|
398
402
|
act(() => {
|
|
399
403
|
app.maintained = true;
|
|
400
404
|
app.maintaining = true;
|
|
401
|
-
app.error = { code: 'APP_COMMANDING', command: { name:
|
|
405
|
+
app.error = { code: 'APP_COMMANDING', command: { name: commandName }, message: 'starting sub applications...' };
|
|
402
406
|
});
|
|
403
407
|
|
|
404
408
|
expect(screen.getByRole('button', { name: 'Current page count: 1' })).toBeInTheDocument();
|
|
405
409
|
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
|
406
|
-
expect(screen.getByText(
|
|
410
|
+
expect(screen.getByText(title)).toBeInTheDocument();
|
|
407
411
|
});
|
|
408
412
|
|
|
409
413
|
it('should keep current content without a dialog while app upgrading', async () => {
|
|
@@ -431,17 +435,22 @@ describe('app', () => {
|
|
|
431
435
|
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
|
432
436
|
});
|
|
433
437
|
|
|
434
|
-
it(
|
|
438
|
+
it.each([
|
|
439
|
+
['upgrade', 'App upgrading'],
|
|
440
|
+
['pm.enable', 'Enabling plugin'],
|
|
441
|
+
['pm.disable', 'Disabling plugin'],
|
|
442
|
+
['future.command', 'future.command'],
|
|
443
|
+
])('should not show Try again for an app commanding error state: %s', async (commandName, title) => {
|
|
435
444
|
class PluginHelloClient extends Plugin {}
|
|
436
445
|
const app = createMockClient({ plugins: [PluginHelloClient] });
|
|
437
446
|
app.error = Object.assign(new Error('Loading data sources...'), {
|
|
438
447
|
code: 'APP_COMMANDING',
|
|
439
|
-
command: { name:
|
|
448
|
+
command: { name: commandName },
|
|
440
449
|
});
|
|
441
450
|
|
|
442
451
|
await renderApp(app);
|
|
443
452
|
|
|
444
|
-
expect(screen.getByText(
|
|
453
|
+
expect(screen.getByText(title)).toBeInTheDocument();
|
|
445
454
|
expect(screen.queryByRole('button', { name: 'Try again' })).not.toBeInTheDocument();
|
|
446
455
|
});
|
|
447
456
|
|
|
@@ -8,9 +8,10 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { ACLRolesCheckProvider, Plugin } from '@nocobase/client-v2';
|
|
11
|
-
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react';
|
|
11
|
+
import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
|
|
12
12
|
import React from 'react';
|
|
13
13
|
import { message } from 'antd';
|
|
14
|
+
import { vi } from 'vitest';
|
|
14
15
|
import { AdminSettingsLayoutModel as ClientV2AdminSettingsLayoutModel } from '../settings-center';
|
|
15
16
|
import { AdminSettingsLayoutModel as ClientV1AdminSettingsLayoutModel } from '../../../client/src/pm/AdminSettingsLayoutModel';
|
|
16
17
|
import zhCN from '../../../client/src/locale/zh-CN.json';
|
|
@@ -253,6 +254,50 @@ describe('settings center', () => {
|
|
|
253
254
|
expect(await screen.findByText('Demo plugin')).toBeInTheDocument();
|
|
254
255
|
});
|
|
255
256
|
|
|
257
|
+
it('should redirect to the admin app when the role cannot access settings', async () => {
|
|
258
|
+
const originalLocation = globalThis.window.location;
|
|
259
|
+
const originalModernClientPrefix = window.__nocobase_modern_client_prefix__;
|
|
260
|
+
const replace = vi.fn();
|
|
261
|
+
Object.defineProperty(globalThis.window, 'location', {
|
|
262
|
+
configurable: true,
|
|
263
|
+
value: {
|
|
264
|
+
...originalLocation,
|
|
265
|
+
replace,
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
window.__nocobase_modern_client_prefix__ = 'v';
|
|
269
|
+
|
|
270
|
+
try {
|
|
271
|
+
const app = createMockSettingsClient({
|
|
272
|
+
plugins: [SettingsBuildInPlugin, TestAclPlugin],
|
|
273
|
+
router: { type: 'memory', initialEntries: ['/settings/system-settings'] },
|
|
274
|
+
});
|
|
275
|
+
mockAdminRuntime(app, {
|
|
276
|
+
snippets: ['!pm', '!pm.system-settings.system-settings'],
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
await renderApp(app);
|
|
280
|
+
await waitForGetRequests(app, ['/auth:check', 'roles:check']);
|
|
281
|
+
|
|
282
|
+
await waitFor(() => {
|
|
283
|
+
expect(replace).toHaveBeenCalledWith('/v/admin');
|
|
284
|
+
});
|
|
285
|
+
expect(
|
|
286
|
+
screen.queryByRole('heading', { name: 'Your current role cannot access Settings' }),
|
|
287
|
+
).not.toBeInTheDocument();
|
|
288
|
+
} finally {
|
|
289
|
+
Object.defineProperty(globalThis.window, 'location', {
|
|
290
|
+
configurable: true,
|
|
291
|
+
value: originalLocation,
|
|
292
|
+
});
|
|
293
|
+
if (originalModernClientPrefix === undefined) {
|
|
294
|
+
delete window.__nocobase_modern_client_prefix__;
|
|
295
|
+
} else {
|
|
296
|
+
window.__nocobase_modern_client_prefix__ = originalModernClientPrefix;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
|
|
256
301
|
it('should hide plugin-manager menu item when pm snippet is missing', async () => {
|
|
257
302
|
const app = createMockSettingsClient({
|
|
258
303
|
plugins: [SettingsBuildInPlugin, TestAclPlugin],
|
|
@@ -274,7 +319,9 @@ describe('settings center', () => {
|
|
|
274
319
|
plugins: [SettingsBuildInPlugin, TestAclPlugin],
|
|
275
320
|
router: { type: 'memory', initialEntries: ['/settings/unknown'] },
|
|
276
321
|
});
|
|
277
|
-
mockAdminRuntime(app
|
|
322
|
+
mockAdminRuntime(app, {
|
|
323
|
+
snippets: ['!pm', '!pm.system-settings.system-settings'],
|
|
324
|
+
});
|
|
278
325
|
|
|
279
326
|
await renderApp(app);
|
|
280
327
|
await waitForGetRequests(app, ['/auth:check', 'roles:check']);
|
|
@@ -284,6 +331,7 @@ describe('settings center', () => {
|
|
|
284
331
|
expect(
|
|
285
332
|
within(result).getByText('The settings page you requested does not exist or has been removed.'),
|
|
286
333
|
).toBeInTheDocument();
|
|
334
|
+
expect(app.router.state.location.pathname).toBe('/settings/unknown');
|
|
287
335
|
});
|
|
288
336
|
|
|
289
337
|
it('should allow direct access to hidden page without showing menu entry', async () => {
|
|
@@ -311,6 +359,7 @@ describe('settings center', () => {
|
|
|
311
359
|
|
|
312
360
|
expect(await screen.findByText('Hidden settings page')).toBeInTheDocument();
|
|
313
361
|
expect(screen.queryByRole('menuitem', { name: 'Hidden demo' })).not.toBeInTheDocument();
|
|
362
|
+
expect(app.router.state.location.pathname).toBe('/settings/hidden-demo');
|
|
314
363
|
});
|
|
315
364
|
|
|
316
365
|
it('should show route empty state when direct access page has no permission', async () => {
|
|
@@ -344,43 +393,67 @@ describe('settings center', () => {
|
|
|
344
393
|
expect(within(result).getByRole('heading', { name: '当前角色无权访问设置中心' })).toBeInTheDocument();
|
|
345
394
|
expect(within(result).getByText('请切换至有权限的角色,或联系管理员获取访问权限。')).toBeInTheDocument();
|
|
346
395
|
expect(screen.queryByText('Secure settings page')).not.toBeInTheDocument();
|
|
396
|
+
expect(app.router.state.location.pathname).toBe('/settings/secure-demo');
|
|
347
397
|
});
|
|
348
398
|
|
|
349
|
-
it('should
|
|
350
|
-
|
|
399
|
+
it('should redirect a denied settings tab to the first accessible tab', async () => {
|
|
400
|
+
const renderDeniedTab = vi.fn(() => <div>Denied tab content</div>);
|
|
401
|
+
|
|
402
|
+
class ProtectedSettingsTabsPlugin extends Plugin {
|
|
351
403
|
async load() {
|
|
352
|
-
this.pluginSettingsManager.addMenuItem({ key: '
|
|
404
|
+
this.pluginSettingsManager.addMenuItem({ key: 'protected-tabs', title: 'Protected tabs' });
|
|
353
405
|
this.pluginSettingsManager.addPageTabItem({
|
|
354
|
-
menuKey: '
|
|
355
|
-
key: '
|
|
356
|
-
title: '
|
|
357
|
-
|
|
406
|
+
menuKey: 'protected-tabs',
|
|
407
|
+
key: 'a-second',
|
|
408
|
+
title: 'Second accessible tab',
|
|
409
|
+
aclSnippet: 'pm.protected-tabs.second',
|
|
410
|
+
sort: 20,
|
|
411
|
+
Component: () => <div>Second accessible tab content</div>,
|
|
412
|
+
});
|
|
413
|
+
this.pluginSettingsManager.addPageTabItem({
|
|
414
|
+
menuKey: 'protected-tabs',
|
|
415
|
+
key: 'z-first',
|
|
416
|
+
title: 'First accessible tab',
|
|
417
|
+
aclSnippet: 'pm.protected-tabs.first',
|
|
418
|
+
sort: 10,
|
|
419
|
+
Component: () => <div>First accessible tab content</div>,
|
|
358
420
|
});
|
|
359
421
|
this.pluginSettingsManager.addPageTabItem({
|
|
360
|
-
menuKey: '
|
|
361
|
-
key: '
|
|
362
|
-
title: '
|
|
363
|
-
aclSnippet: 'pm.
|
|
364
|
-
|
|
422
|
+
menuKey: 'protected-tabs',
|
|
423
|
+
key: 'denied',
|
|
424
|
+
title: 'Denied tab',
|
|
425
|
+
aclSnippet: 'pm.protected-tabs.denied',
|
|
426
|
+
sort: 30,
|
|
427
|
+
Component: renderDeniedTab,
|
|
365
428
|
});
|
|
366
429
|
}
|
|
367
430
|
}
|
|
368
431
|
|
|
369
432
|
const app = createMockSettingsClient({
|
|
370
|
-
plugins: [SettingsBuildInPlugin, TestAclPlugin,
|
|
371
|
-
router: { type: 'memory', initialEntries: ['/settings/
|
|
433
|
+
plugins: [SettingsBuildInPlugin, TestAclPlugin, ProtectedSettingsTabsPlugin],
|
|
434
|
+
router: { type: 'memory', initialEntries: ['/settings/protected-tabs/denied'] },
|
|
372
435
|
});
|
|
373
436
|
mockAdminRuntime(app, {
|
|
374
|
-
snippets: ['pm', '
|
|
437
|
+
snippets: ['pm', '!pm.protected-tabs.denied'],
|
|
375
438
|
});
|
|
376
439
|
|
|
377
440
|
await renderApp(app);
|
|
378
441
|
await waitForGetRequests(app, ['/auth:check', 'roles:check']);
|
|
379
442
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
)
|
|
383
|
-
expect(
|
|
443
|
+
await waitFor(() => {
|
|
444
|
+
expect(app.router.router.state.location.pathname).toBe('/settings/protected-tabs/z-first');
|
|
445
|
+
});
|
|
446
|
+
expect(app.router.router.state.historyAction).toBe('REPLACE');
|
|
447
|
+
expect(await screen.findByText('First accessible tab content')).toBeInTheDocument();
|
|
448
|
+
expect(screen.queryByText('Denied tab content')).not.toBeInTheDocument();
|
|
449
|
+
expect(renderDeniedTab).not.toHaveBeenCalled();
|
|
450
|
+
|
|
451
|
+
await act(async () => {
|
|
452
|
+
await app.router.router.navigate('/settings/protected-tabs/a-second');
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
expect(await screen.findByText('Second accessible tab content')).toBeInTheDocument();
|
|
456
|
+
expect(app.router.router.state.location.pathname).toBe('/settings/protected-tabs/a-second');
|
|
384
457
|
});
|
|
385
458
|
|
|
386
459
|
it.each(['/settings/fallback-access', '/settings/fallback-access/'])(
|
|
@@ -424,6 +497,134 @@ describe('settings center', () => {
|
|
|
424
497
|
},
|
|
425
498
|
);
|
|
426
499
|
|
|
500
|
+
it('should skip an accessible dynamic tab when its route params cannot be resolved', async () => {
|
|
501
|
+
class DynamicSettingsTabsPlugin extends Plugin {
|
|
502
|
+
async load() {
|
|
503
|
+
this.pluginSettingsManager.addMenuItem({ key: 'dynamic-tabs', title: 'Dynamic tabs' });
|
|
504
|
+
this.pluginSettingsManager.addPageTabItem({
|
|
505
|
+
menuKey: 'dynamic-tabs',
|
|
506
|
+
key: ':name',
|
|
507
|
+
title: 'Dynamic tab',
|
|
508
|
+
aclSnippet: 'pm.dynamic-tabs.dynamic',
|
|
509
|
+
sort: 1,
|
|
510
|
+
Component: () => <div>Dynamic tab content</div>,
|
|
511
|
+
});
|
|
512
|
+
this.pluginSettingsManager.addPageTabItem({
|
|
513
|
+
menuKey: 'dynamic-tabs',
|
|
514
|
+
key: 'fallback',
|
|
515
|
+
title: 'Static fallback tab',
|
|
516
|
+
aclSnippet: 'pm.dynamic-tabs.fallback',
|
|
517
|
+
sort: 2,
|
|
518
|
+
Component: () => <div>Static fallback tab content</div>,
|
|
519
|
+
});
|
|
520
|
+
this.pluginSettingsManager.addPageTabItem({
|
|
521
|
+
menuKey: 'dynamic-tabs',
|
|
522
|
+
key: 'denied',
|
|
523
|
+
title: 'Denied tab',
|
|
524
|
+
aclSnippet: 'pm.dynamic-tabs.denied',
|
|
525
|
+
sort: 3,
|
|
526
|
+
Component: () => <div>Denied dynamic tab content</div>,
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const app = createMockSettingsClient({
|
|
532
|
+
plugins: [SettingsBuildInPlugin, TestAclPlugin, DynamicSettingsTabsPlugin],
|
|
533
|
+
router: { type: 'memory', initialEntries: ['/settings/dynamic-tabs/denied'] },
|
|
534
|
+
});
|
|
535
|
+
mockAdminRuntime(app, {
|
|
536
|
+
snippets: ['pm', '!pm.dynamic-tabs.denied'],
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
await renderApp(app);
|
|
540
|
+
await waitForGetRequests(app, ['/auth:check', 'roles:check']);
|
|
541
|
+
|
|
542
|
+
await waitFor(() => {
|
|
543
|
+
expect(app.router.router.state.location.pathname).toBe('/settings/dynamic-tabs/fallback');
|
|
544
|
+
});
|
|
545
|
+
expect(await screen.findByText('Static fallback tab content')).toBeInTheDocument();
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
it('should preserve resolved route params when redirecting between dynamic tabs', async () => {
|
|
549
|
+
class DynamicSiblingTabsPlugin extends Plugin {
|
|
550
|
+
async load() {
|
|
551
|
+
this.pluginSettingsManager.addMenuItem({ key: 'dynamic-siblings', title: 'Dynamic sibling tabs' });
|
|
552
|
+
this.pluginSettingsManager.addPageTabItem({
|
|
553
|
+
menuKey: 'dynamic-siblings',
|
|
554
|
+
key: ':name/channels',
|
|
555
|
+
title: 'Dynamic channels tab',
|
|
556
|
+
aclSnippet: 'pm.dynamic-siblings.channels',
|
|
557
|
+
sort: 1,
|
|
558
|
+
Component: () => <div>Dynamic channels tab content</div>,
|
|
559
|
+
});
|
|
560
|
+
this.pluginSettingsManager.addPageTabItem({
|
|
561
|
+
menuKey: 'dynamic-siblings',
|
|
562
|
+
key: ':name/logs',
|
|
563
|
+
title: 'Dynamic logs tab',
|
|
564
|
+
aclSnippet: 'pm.dynamic-siblings.logs',
|
|
565
|
+
sort: 2,
|
|
566
|
+
Component: () => <div>Dynamic logs tab content</div>,
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const app = createMockSettingsClient({
|
|
572
|
+
plugins: [SettingsBuildInPlugin, TestAclPlugin, DynamicSiblingTabsPlugin],
|
|
573
|
+
router: { type: 'memory', initialEntries: ['/settings/dynamic-siblings/email:primary/logs'] },
|
|
574
|
+
});
|
|
575
|
+
mockAdminRuntime(app, {
|
|
576
|
+
snippets: ['pm', '!pm.dynamic-siblings.logs'],
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
await renderApp(app);
|
|
580
|
+
await waitForGetRequests(app, ['/auth:check', 'roles:check']);
|
|
581
|
+
|
|
582
|
+
await waitFor(() => {
|
|
583
|
+
expect(app.router.router.state.location.pathname).toBe('/settings/dynamic-siblings/email:primary/channels');
|
|
584
|
+
});
|
|
585
|
+
expect(await screen.findByText('Dynamic channels tab content')).toBeInTheDocument();
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
it('should resolve hyphenated route param names when redirecting between dynamic tabs', async () => {
|
|
589
|
+
class HyphenatedParamTabsPlugin extends Plugin {
|
|
590
|
+
async load() {
|
|
591
|
+
this.pluginSettingsManager.addMenuItem({ key: 'hyphenated-params', title: 'Hyphenated param tabs' });
|
|
592
|
+
this.pluginSettingsManager.addPageTabItem({
|
|
593
|
+
menuKey: 'hyphenated-params',
|
|
594
|
+
key: ':data-source/channels',
|
|
595
|
+
title: 'Hyphenated channels tab',
|
|
596
|
+
aclSnippet: 'pm.hyphenated-params.channels',
|
|
597
|
+
sort: 1,
|
|
598
|
+
Component: () => <div>Hyphenated channels tab content</div>,
|
|
599
|
+
});
|
|
600
|
+
this.pluginSettingsManager.addPageTabItem({
|
|
601
|
+
menuKey: 'hyphenated-params',
|
|
602
|
+
key: ':data-source/logs',
|
|
603
|
+
title: 'Hyphenated logs tab',
|
|
604
|
+
aclSnippet: 'pm.hyphenated-params.logs',
|
|
605
|
+
sort: 2,
|
|
606
|
+
Component: () => <div>Hyphenated logs tab content</div>,
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const app = createMockSettingsClient({
|
|
612
|
+
plugins: [SettingsBuildInPlugin, TestAclPlugin, HyphenatedParamTabsPlugin],
|
|
613
|
+
router: { type: 'memory', initialEntries: ['/settings/hyphenated-params/email/logs'] },
|
|
614
|
+
});
|
|
615
|
+
mockAdminRuntime(app, {
|
|
616
|
+
snippets: ['pm', '!pm.hyphenated-params.logs'],
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
await renderApp(app);
|
|
620
|
+
await waitForGetRequests(app, ['/auth:check', 'roles:check']);
|
|
621
|
+
|
|
622
|
+
await waitFor(() => {
|
|
623
|
+
expect(app.router.router.state.location.pathname).toBe('/settings/hyphenated-params/email/channels');
|
|
624
|
+
});
|
|
625
|
+
expect(await screen.findByText('Hyphenated channels tab content')).toBeInTheDocument();
|
|
626
|
+
});
|
|
627
|
+
|
|
427
628
|
it('should keep menu visible when menu acl is denied but child page is visible', async () => {
|
|
428
629
|
class MenuAclPlugin extends Plugin {
|
|
429
630
|
async load() {
|
|
@@ -30,8 +30,8 @@ interface AppErrorPayload {
|
|
|
30
30
|
};
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
const
|
|
34
|
-
|
|
33
|
+
const isAppCommanding = (error?: AppErrorPayload) => error?.code === 'APP_COMMANDING';
|
|
34
|
+
const isAppUpgrading = (error?: AppErrorPayload) => isAppCommanding(error) && error?.command?.name === 'upgrade';
|
|
35
35
|
|
|
36
36
|
export const AppSpin = () => {
|
|
37
37
|
return (
|
|
@@ -170,7 +170,7 @@ const getProps = (app: Application) => {
|
|
|
170
170
|
export const AppError: FC<{ error: Error & { title?: string }; app: Application }> = observer(
|
|
171
171
|
({ app, error }) => {
|
|
172
172
|
const props = getProps(app);
|
|
173
|
-
const
|
|
173
|
+
const commanding = isAppCommanding(app.error as AppErrorPayload | undefined);
|
|
174
174
|
return (
|
|
175
175
|
<div>
|
|
176
176
|
<Result
|
|
@@ -184,7 +184,7 @@ export const AppError: FC<{ error: Error & { title?: string }; app: Application
|
|
|
184
184
|
title={error?.title || app.i18n.t('App error', { ns: 'client' })}
|
|
185
185
|
subTitle={app.i18n.t(error?.message)}
|
|
186
186
|
extra={
|
|
187
|
-
|
|
187
|
+
commanding ? null : (
|
|
188
188
|
<Button type="primary" key="try" onClick={() => window.location.reload()}>
|
|
189
189
|
{app.i18n.t('Try again')}
|
|
190
190
|
</Button>
|
|
@@ -0,0 +1,33 @@
|
|
|
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 { describe, expect, it } from 'vitest';
|
|
11
|
+
import { getActionLinkageStateOptions } from '../linkageRules';
|
|
12
|
+
|
|
13
|
+
describe('getActionLinkageStateOptions', () => {
|
|
14
|
+
const t = (key: string) => key;
|
|
15
|
+
|
|
16
|
+
it('returns all states for regular action models', () => {
|
|
17
|
+
expect(getActionLinkageStateOptions({}, t).map((option) => option.value)).toEqual([
|
|
18
|
+
'visible',
|
|
19
|
+
'hidden',
|
|
20
|
+
'hiddenText',
|
|
21
|
+
'enabled',
|
|
22
|
+
'disabled',
|
|
23
|
+
]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('filters states declared unsupported by the action model', () => {
|
|
27
|
+
expect(
|
|
28
|
+
getActionLinkageStateOptions({ supportedActionLinkageStates: ['visible', 'hidden'] }, t).map(
|
|
29
|
+
(option) => option.value,
|
|
30
|
+
),
|
|
31
|
+
).toEqual(['visible', 'hidden']);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -660,6 +660,30 @@ export const linkageSetBlockProps = defineAction({
|
|
|
660
660
|
},
|
|
661
661
|
});
|
|
662
662
|
|
|
663
|
+
const ACTION_LINKAGE_STATE_OPTIONS = [
|
|
664
|
+
{ label: 'Visible', value: 'visible' },
|
|
665
|
+
{ label: 'Hidden', value: 'hidden' },
|
|
666
|
+
{ label: 'Hidden text', value: 'hiddenText' },
|
|
667
|
+
{ label: 'Enabled', value: 'enabled' },
|
|
668
|
+
{ label: 'Disabled', value: 'disabled' },
|
|
669
|
+
] as const;
|
|
670
|
+
|
|
671
|
+
type ActionLinkageState = (typeof ACTION_LINKAGE_STATE_OPTIONS)[number]['value'];
|
|
672
|
+
|
|
673
|
+
export function getActionLinkageStateOptions(
|
|
674
|
+
model: { supportedActionLinkageStates?: readonly ActionLinkageState[] },
|
|
675
|
+
t: (key: string) => string,
|
|
676
|
+
) {
|
|
677
|
+
const supportedStates = model.supportedActionLinkageStates ? new Set(model.supportedActionLinkageStates) : undefined;
|
|
678
|
+
|
|
679
|
+
return ACTION_LINKAGE_STATE_OPTIONS.filter((option) => !supportedStates || supportedStates.has(option.value)).map(
|
|
680
|
+
(option) => ({
|
|
681
|
+
label: t(option.label),
|
|
682
|
+
value: option.value,
|
|
683
|
+
}),
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
|
|
663
687
|
export const linkageSetActionProps = defineAction({
|
|
664
688
|
name: 'linkageSetActionProps',
|
|
665
689
|
title: tExpr('Set button state'),
|
|
@@ -680,13 +704,7 @@ export const linkageSetActionProps = defineAction({
|
|
|
680
704
|
onChange={onChange}
|
|
681
705
|
placeholder={t('Please select state')}
|
|
682
706
|
style={{ width: '100%' }}
|
|
683
|
-
options={
|
|
684
|
-
{ label: t('Visible'), value: 'visible' },
|
|
685
|
-
{ label: t('Hidden'), value: 'hidden' },
|
|
686
|
-
{ label: t('Hidden text'), value: 'hiddenText' },
|
|
687
|
-
{ label: t('Enabled'), value: 'enabled' },
|
|
688
|
-
{ label: t('Disabled'), value: 'disabled' },
|
|
689
|
-
]}
|
|
707
|
+
options={getActionLinkageStateOptions(ctx.model, t)}
|
|
690
708
|
allowClear
|
|
691
709
|
/>
|
|
692
710
|
);
|