@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.
Files changed (40) hide show
  1. package/es/flow/actions/linkageRules.d.ts +24 -0
  2. package/es/flow/components/FieldAssignValueInput.d.ts +4 -30
  3. package/es/flow/components/field-value-variable/DateVariableEditor.d.ts +24 -0
  4. package/es/flow/components/field-value-variable/FieldValueVariableInput.d.ts +31 -0
  5. package/es/flow/components/field-value-variable/dateValue.d.ts +36 -0
  6. package/es/flow/components/field-value-variable/index.d.ts +11 -0
  7. package/es/flow/models/blocks/form/FormBlockModel.d.ts +7 -0
  8. package/es/flow/models/blocks/form/FormGridModel.d.ts +6 -0
  9. package/es/flow/models/blocks/form/value-runtime/rules.d.ts +1 -0
  10. package/es/index.mjs +125 -134
  11. package/lib/index.js +119 -128
  12. package/package.json +7 -7
  13. package/src/__tests__/app.test.tsx +15 -6
  14. package/src/__tests__/settings-center.test.tsx +222 -21
  15. package/src/components/AppComponents.tsx +4 -4
  16. package/src/flow/actions/__tests__/linkageRules.actionStates.test.ts +33 -0
  17. package/src/flow/actions/linkageRules.tsx +25 -7
  18. package/src/flow/components/FieldAssignValueInput.tsx +38 -621
  19. package/src/flow/components/field-value-variable/DateVariableEditor.tsx +181 -0
  20. package/src/flow/components/field-value-variable/FieldValueVariableInput.tsx +326 -0
  21. package/src/flow/components/field-value-variable/__tests__/FieldValueVariableInput.test.tsx +380 -0
  22. package/src/flow/components/field-value-variable/dateValue.ts +223 -0
  23. package/src/flow/components/field-value-variable/index.ts +12 -0
  24. package/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx +28 -53
  25. package/src/flow/models/blocks/form/FormBlockModel.tsx +47 -0
  26. package/src/flow/models/blocks/form/FormGridModel.tsx +4 -0
  27. package/src/flow/models/blocks/form/__tests__/runJsFormSubmit.test.ts +131 -0
  28. package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +435 -0
  29. package/src/flow/models/blocks/form/value-runtime/rules.ts +67 -14
  30. package/src/flow/models/blocks/form/value-runtime/runtime.ts +43 -19
  31. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +8 -0
  32. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/__tests__/popupContext.test.ts +120 -0
  33. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/actions/PopupSubTableEditActionModel.tsx +10 -2
  34. package/src/flow/models/fields/DisplayNumberFieldModel.tsx +1 -1
  35. package/src/flow/models/fields/NumberFieldModel.tsx +1 -1
  36. package/src/flow/models/fields/__tests__/DisplayNumberFieldModel.test.ts +33 -0
  37. package/src/flow/models/fields/__tests__/NumberFieldModel.test.tsx +47 -0
  38. package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +1 -0
  39. package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +20 -2
  40. 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.7",
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.7",
31
- "@nocobase/flow-engine": "3.0.0-alpha.7",
32
- "@nocobase/sdk": "3.0.0-alpha.7",
33
- "@nocobase/shared": "3.0.0-alpha.7",
34
- "@nocobase/utils": "3.0.0-alpha.7",
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": "19cd28c710f3b3c614ebe2fd7d5a415d527ee559"
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('should keep current content behind maintained dialog state', async () => {
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: 'pm.enable' }, message: 'starting sub applications...' };
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('Enabling plugin')).toBeInTheDocument();
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('should not show Try again for an app upgrade error state', async () => {
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: 'upgrade' },
448
+ command: { name: commandName },
440
449
  });
441
450
 
442
451
  await renderApp(app);
443
452
 
444
- expect(screen.getByText('App upgrading')).toBeInTheDocument();
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 deny a protected child page when a sibling remains visible', async () => {
350
- class MixedAccessSettingsPlugin extends Plugin {
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: 'mixed-access', title: 'Mixed access' });
404
+ this.pluginSettingsManager.addMenuItem({ key: 'protected-tabs', title: 'Protected tabs' });
353
405
  this.pluginSettingsManager.addPageTabItem({
354
- menuKey: 'mixed-access',
355
- key: 'index',
356
- title: 'Allowed settings',
357
- Component: () => <div>Allowed settings page</div>,
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: 'mixed-access',
361
- key: 'restricted',
362
- title: 'Restricted settings',
363
- aclSnippet: 'pm.mixed-access.restricted',
364
- Component: () => <div>Restricted settings page</div>,
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, MixedAccessSettingsPlugin],
371
- router: { type: 'memory', initialEntries: ['/settings/mixed-access/restricted'] },
433
+ plugins: [SettingsBuildInPlugin, TestAclPlugin, ProtectedSettingsTabsPlugin],
434
+ router: { type: 'memory', initialEntries: ['/settings/protected-tabs/denied'] },
372
435
  });
373
436
  mockAdminRuntime(app, {
374
- snippets: ['pm', 'pm.system-settings.system-settings', '!pm.mixed-access.restricted'],
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
- expect(
381
- await screen.findByRole('heading', { name: 'Your current role cannot access Settings' }),
382
- ).toBeInTheDocument();
383
- expect(screen.queryByText('Restricted settings page')).not.toBeInTheDocument();
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 isAppUpgrading = (error?: AppErrorPayload) =>
34
- error?.code === 'APP_COMMANDING' && error.command?.name === 'upgrade';
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 upgrading = isAppUpgrading(app.error as AppErrorPayload | undefined);
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
- upgrading ? null : (
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
  );