@nocobase/client-v2 2.2.0-beta.14 → 2.2.0-beta.16

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 (114) hide show
  1. package/es/components/category-tabs/SortableCategoryTabs.d.ts +55 -0
  2. package/es/components/category-tabs/index.d.ts +9 -0
  3. package/es/components/form/ScanInput/useCodeScanner.d.ts +2 -2
  4. package/es/components/form/TypedVariableInput.d.ts +31 -5
  5. package/es/components/form/filter/CollectionFilter.d.ts +2 -0
  6. package/es/components/form/filter/CollectionFilterItem.d.ts +11 -1
  7. package/es/components/form/filter/CollectionFilterPanel.d.ts +2 -0
  8. package/es/components/form/filter/index.d.ts +2 -0
  9. package/es/components/form/filter/useFilterActionProps.d.ts +2 -0
  10. package/es/components/index.d.ts +1 -0
  11. package/es/flow/actions/index.d.ts +1 -1
  12. package/es/flow/actions/linkageRules.d.ts +3 -0
  13. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +1 -0
  14. package/es/flow/components/FieldAssignExactDatePicker.d.ts +1 -0
  15. package/es/flow/components/FieldAssignValueInput.d.ts +9 -0
  16. package/es/flow/components/FlowRoute.d.ts +2 -2
  17. package/es/flow/components/RunJSValueEditor.d.ts +1 -0
  18. package/es/flow/components/filter/FilterGroup.d.ts +1 -0
  19. package/es/flow/components/filter/VariableFilterItem.d.ts +6 -1
  20. package/es/flow/index.d.ts +1 -0
  21. package/es/flow/internal/registerDeviceTypeContext.d.ts +10 -0
  22. package/es/flow/models/base/ActionModelCore.d.ts +4 -2
  23. package/es/flow/models/base/BlockGridModel.d.ts +3 -0
  24. package/es/flow/models/base/PageModel/PageModel.d.ts +12 -0
  25. package/es/flow/models/base/PageModel/PageModelTabBar.d.ts +22 -0
  26. package/es/flow/models/base/PageModel/PageTabModel.d.ts +12 -0
  27. package/es/flow/models/blocks/table/TableColumnModel.d.ts +1 -0
  28. package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +15 -7
  29. package/es/flow-compat/routeTypes.d.ts +1 -0
  30. package/es/index.d.ts +4 -0
  31. package/es/index.mjs +143 -106
  32. package/es/utils/getRouteRuntimeVersion.d.ts +23 -0
  33. package/es/utils/index.d.ts +1 -0
  34. package/lib/index.js +154 -117
  35. package/lib/locale/languageCodes.js +3 -1
  36. package/package.json +8 -7
  37. package/src/__tests__/browserChecker.test.ts +61 -0
  38. package/src/__tests__/getRouteRuntimeVersion.test.ts +68 -0
  39. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +7 -2
  40. package/src/components/README.md +7 -1
  41. package/src/components/README.zh-CN.md +6 -1
  42. package/src/components/category-tabs/SortableCategoryTabs.tsx +210 -0
  43. package/src/components/category-tabs/index.ts +10 -0
  44. package/src/components/form/JsonTextArea.tsx +4 -0
  45. package/src/components/form/ScanInput/CodeScanner.tsx +61 -34
  46. package/src/components/form/ScanInput/__tests__/CodeScanner.test.tsx +101 -0
  47. package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +114 -18
  48. package/src/components/form/ScanInput/useCodeScanner.ts +166 -23
  49. package/src/components/form/TypedVariableInput.tsx +452 -101
  50. package/src/components/form/__tests__/TypedVariableInput.test.tsx +364 -12
  51. package/src/components/form/filter/CollectionFilter.tsx +4 -0
  52. package/src/components/form/filter/CollectionFilterItem.tsx +32 -7
  53. package/src/components/form/filter/CollectionFilterPanel.tsx +4 -0
  54. package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +38 -0
  55. package/src/components/form/filter/__tests__/useFilterActionProps.test.tsx +95 -0
  56. package/src/components/form/filter/index.ts +2 -0
  57. package/src/components/form/filter/useFilterActionProps.ts +37 -6
  58. package/src/components/index.ts +1 -0
  59. package/src/flow/__tests__/FlowRoute.test.tsx +126 -8
  60. package/src/flow/__tests__/PluginFlowEngine.test.ts +58 -0
  61. package/src/flow/actions/__tests__/actionLinkageRules.forkProps.test.ts +314 -0
  62. package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +102 -0
  63. package/src/flow/actions/__tests__/linkageRules.tab.test.ts +171 -0
  64. package/src/flow/actions/index.ts +2 -0
  65. package/src/flow/actions/linkageRules.tsx +111 -16
  66. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +0 -1
  67. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +253 -6
  68. package/src/flow/common/Markdown/style.ts +4 -0
  69. package/src/flow/components/BlockItemCard.tsx +2 -2
  70. package/src/flow/components/FieldAssignExactDatePicker.tsx +25 -11
  71. package/src/flow/components/FieldAssignValueInput.tsx +76 -9
  72. package/src/flow/components/FlowRoute.tsx +71 -29
  73. package/src/flow/components/RunJSValueEditor.tsx +9 -1
  74. package/src/flow/components/__tests__/FieldAssignValueInput.context.test.tsx +84 -2
  75. package/src/flow/components/filter/FilterGroup.tsx +33 -5
  76. package/src/flow/components/filter/VariableFilterItem.tsx +158 -10
  77. package/src/flow/components/filter/__tests__/FilterGroup.test.tsx +45 -0
  78. package/src/flow/components/filter/__tests__/VariableFilterItem.leftMetaTree.test.tsx +9 -0
  79. package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +70 -1
  80. package/src/flow/index.ts +3 -0
  81. package/src/flow/internal/registerDeviceTypeContext.ts +39 -0
  82. package/src/flow/models/base/ActionModelCore.tsx +11 -2
  83. package/src/flow/models/base/BlockGridModel.tsx +26 -0
  84. package/src/flow/models/base/CollectionBlockModel.tsx +6 -2
  85. package/src/flow/models/base/PageModel/PageModel.tsx +387 -53
  86. package/src/flow/models/base/PageModel/PageModelTabBar.tsx +114 -0
  87. package/src/flow/models/base/PageModel/PageTabModel.tsx +249 -18
  88. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +790 -10
  89. package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.module-isolation.test.tsx +130 -0
  90. package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.test.tsx +118 -0
  91. package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +1075 -7
  92. package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +49 -0
  93. package/src/flow/models/base/__tests__/BlockGridModel.selectSceneActivation.test.ts +124 -0
  94. package/src/flow/models/base/__tests__/CollectionBlockModel.addAppends.test.ts +41 -0
  95. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +27 -3
  96. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +32 -4
  97. package/src/flow/models/blocks/js-block/JSBlock.tsx +1 -1
  98. package/src/flow/models/blocks/table/TableBlockModel.tsx +3 -1
  99. package/src/flow/models/blocks/table/TableColumnModel.tsx +30 -1
  100. package/src/flow/models/blocks/table/__tests__/TableBlockModel.quickEditRefresh.test.ts +12 -0
  101. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +33 -0
  102. package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +149 -0
  103. package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +149 -113
  104. package/src/flow/models/fields/JsonFieldModel.tsx +31 -8
  105. package/src/flow/models/fields/VariableFieldFormModel.tsx +3 -3
  106. package/src/flow/models/fields/__tests__/JsonFieldModel.test.ts +82 -0
  107. package/src/flow/models/fields/__tests__/VariableFieldFormModel.test.tsx +51 -0
  108. package/src/flow/utils/dataScopeFormValueClear.ts +4 -3
  109. package/src/flow-compat/routeTypes.ts +1 -0
  110. package/src/index.ts +4 -0
  111. package/src/locale/languageCodes.ts +3 -1
  112. package/src/nocobase-buildin-plugin/index.tsx +2 -0
  113. package/src/utils/getRouteRuntimeVersion.ts +185 -0
  114. package/src/utils/index.tsx +1 -0
@@ -68,6 +68,12 @@ interface LinkageRule {
68
68
  }[];
69
69
  }
70
70
 
71
+ export function updateLinkageRules<T>(rules: T[], updater: (nextRules: T[]) => void): T[] {
72
+ const nextRules = _.cloneDeep(rules);
73
+ updater(nextRules);
74
+ return nextRules;
75
+ }
76
+
71
77
  const previewValueForLog = (value: any) => {
72
78
  if (value == null) return value;
73
79
  const t = typeof value;
@@ -731,6 +737,41 @@ export const linkageSetMenuItemProps = defineAction({
731
737
  },
732
738
  });
733
739
 
740
+ export const linkageSetTabProps = defineAction({
741
+ name: 'linkageSetTabProps',
742
+ title: tExpr('Set tab state'),
743
+ scene: ActionScene.TAB_LINKAGE_RULES,
744
+ sort: 100,
745
+ uiSchema: {
746
+ value: {
747
+ type: 'string',
748
+ 'x-component': (props) => {
749
+ const { value, onChange } = props;
750
+ // eslint-disable-next-line react-hooks/rules-of-hooks
751
+ const ctx = useFlowContext();
752
+ const t = ctx.model.translate.bind(ctx.model);
753
+
754
+ return (
755
+ <Select
756
+ value={value}
757
+ onChange={onChange}
758
+ placeholder={t('Please select state')}
759
+ style={{ width: '100%' }}
760
+ options={[
761
+ { label: t('Visible'), value: 'visible' },
762
+ { label: t('Hidden'), value: 'hidden' },
763
+ ]}
764
+ allowClear
765
+ />
766
+ );
767
+ },
768
+ },
769
+ },
770
+ handler(ctx, { value, setProps }) {
771
+ setProps(ctx.model, { hiddenModel: value === 'hidden' });
772
+ },
773
+ });
774
+
734
775
  export const linkageSetFieldProps = defineAction({
735
776
  name: 'linkageSetFieldProps',
736
777
  title: tExpr('Set field state'),
@@ -1339,6 +1380,7 @@ export const linkageRunjs = defineAction({
1339
1380
  ActionScene.FIELD_LINKAGE_RULES,
1340
1381
  ActionScene.ACTION_LINKAGE_RULES,
1341
1382
  ActionScene.MENU_LINKAGE_RULES,
1383
+ ActionScene.TAB_LINKAGE_RULES,
1342
1384
  ActionScene.DETAILS_FIELD_LINKAGE_RULES,
1343
1385
  ActionScene.SUB_FORM_FIELD_LINKAGE_RULES,
1344
1386
  ],
@@ -1464,13 +1506,26 @@ async function resolveLinkageRulesParamsPreservingRunJsScripts(ctx: FlowContext,
1464
1506
  }
1465
1507
 
1466
1508
  const LinkageRulesUI = observer(
1467
- (props: { readonly value: LinkageRule[]; supportedActions: string[]; title?: string }) => {
1468
- const { value: rules, supportedActions } = props;
1509
+ (props: {
1510
+ readonly value: LinkageRule[];
1511
+ onChange?: (value: LinkageRule[]) => void;
1512
+ supportedActions: string[];
1513
+ title?: string;
1514
+ }) => {
1515
+ const { value: rules = [], onChange, supportedActions } = props;
1469
1516
  const ctx = useFlowContext();
1470
1517
  const flowEngine = useFlowEngine();
1471
1518
  const t = ctx.model.translate.bind(ctx.model);
1472
1519
  const assignPriorityTip = t('Assignment takes precedence over form field assignment');
1473
1520
 
1521
+ const replaceRules = (updater: (nextRules: LinkageRule[]) => void) => {
1522
+ if (onChange) {
1523
+ onChange(updateLinkageRules(rules, updater));
1524
+ } else {
1525
+ updater(rules);
1526
+ }
1527
+ };
1528
+
1474
1529
  // 创建新规则的默认值
1475
1530
  const createNewRule = (): LinkageRule => ({
1476
1531
  key: uid(),
@@ -1487,7 +1542,7 @@ const LinkageRulesUI = observer(
1487
1542
 
1488
1543
  // 删除规则
1489
1544
  const handleDeleteRule = (index: number) => {
1490
- rules.splice(index, 1);
1545
+ replaceRules((nextRules) => nextRules.splice(index, 1));
1491
1546
  };
1492
1547
 
1493
1548
  // 上移规则
@@ -1526,7 +1581,9 @@ const LinkageRulesUI = observer(
1526
1581
 
1527
1582
  // 切换规则启用状态
1528
1583
  const handleToggleEnable = (index: number, enable: boolean) => {
1529
- rules[index].enable = enable;
1584
+ replaceRules((nextRules) => {
1585
+ nextRules[index].enable = enable;
1586
+ });
1530
1587
  };
1531
1588
 
1532
1589
  // 获取可用的动作类型
@@ -2169,17 +2226,30 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
2169
2226
  }
2170
2227
  : props;
2171
2228
 
2172
- // 存储原始值,用于恢复
2173
- if (!model.__originalProps) {
2174
- model.__originalProps = {
2175
- hiddenModel: model.hidden,
2176
- hiddenText: undefined,
2177
- disabled: undefined,
2178
- required: undefined,
2179
- hidden: undefined,
2180
- ...model.props,
2181
- };
2182
- }
2229
+ // 只记录联动实际控制的属性,避免之后恢复状态时把标题、路由等无关的新配置回滚到旧快照。
2230
+ const originalProps = model.__originalProps || (model.__originalProps = {});
2231
+ const rememberOriginalProp = (key: string, value: unknown) => {
2232
+ if (!Object.prototype.hasOwnProperty.call(originalProps, key)) {
2233
+ originalProps[key] = value;
2234
+ }
2235
+ };
2236
+ Object.keys(normalizedProps || {}).forEach((key) => {
2237
+ if (key === 'hiddenModel') {
2238
+ rememberOriginalProp(
2239
+ key,
2240
+ Object.prototype.hasOwnProperty.call(model.props || {}, key) ? model.props?.[key] : model.hidden,
2241
+ );
2242
+ return;
2243
+ }
2244
+
2245
+ rememberOriginalProp(key, model.props?.[key]);
2246
+ if (key === 'hiddenText' && normalizedProps[key]) {
2247
+ rememberOriginalProp('title', model.props?.title);
2248
+ }
2249
+ if (key === 'required') {
2250
+ rememberOriginalProp('rules', model.props?.rules);
2251
+ }
2252
+ });
2183
2253
 
2184
2254
  // 临时存起来,遍历完所有规则后,再统一处理
2185
2255
  patchPropsByModel.set(model, {
@@ -2237,7 +2307,6 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
2237
2307
  const newProps = { ...model.__originalProps, ...patchProps };
2238
2308
  const prevHidden = !!model.hidden;
2239
2309
  const nextHidden = !!newProps.hiddenModel;
2240
-
2241
2310
  model.setProps(_.omit(newProps, ['hiddenModel', 'value', 'hiddenText', LINKAGE_ASSIGN_MODE_PROP]));
2242
2311
  syncFieldOptionsToForks(model, patchProps);
2243
2312
  if (typeof model.setHidden === 'function') {
@@ -2463,6 +2532,32 @@ export const menuLinkageRules = defineAction({
2463
2532
  },
2464
2533
  });
2465
2534
 
2535
+ export const tabLinkageRules = defineAction({
2536
+ name: 'tabLinkageRules',
2537
+ title: tExpr('Tab linkage rules'),
2538
+ uiMode: 'embed',
2539
+ uiSchema(ctx) {
2540
+ return {
2541
+ value: {
2542
+ type: 'array',
2543
+ 'x-component': LinkageRulesUI,
2544
+ 'x-component-props': {
2545
+ supportedActions: getSupportedActions(ctx, ActionScene.TAB_LINKAGE_RULES),
2546
+ title: tExpr('Tab linkage rules'),
2547
+ },
2548
+ },
2549
+ };
2550
+ },
2551
+ defaultParams: {
2552
+ value: [],
2553
+ },
2554
+ useRawParams: true,
2555
+ handler: async (ctx, params) => {
2556
+ const resolved = await resolveLinkageRulesParamsPreservingRunJsScripts(ctx, params);
2557
+ return commonLinkageRulesHandler(ctx, resolved);
2558
+ },
2559
+ });
2560
+
2466
2561
  export const fieldLinkageRules = defineAction({
2467
2562
  name: 'fieldLinkageRules',
2468
2563
  title: tExpr('Field linkage rules'),
@@ -712,7 +712,6 @@ export const AdminLayoutComponent = observer((props: any) => {
712
712
  <MobileMenuControlContext.Provider value={{ closeMobileMenu }}>
713
713
  <DndProvider collisionDetection={menuCollisionDetection} onDragEnd={handleMenuDragEnd}>
714
714
  <ProLayout
715
- key={`admin-layout-menu-${menuRouteRefreshVersion}`}
716
715
  {...props}
717
716
  contentStyle={contentStyle}
718
717
  siderWidth={customToken.siderWidth || 200}
@@ -11,23 +11,58 @@ import { FlowEngine, FlowEngineProvider } from '@nocobase/flow-engine';
11
11
  import { act, render, screen, waitFor } from '@testing-library/react';
12
12
  import React, { useEffect } from 'react';
13
13
  import { createMemoryRouter, RouterProvider, useParams } from 'react-router-dom';
14
- import { describe, expect, it, vi } from 'vitest';
14
+ import { afterEach, describe, expect, it, vi } from 'vitest';
15
+ import { NocoBaseDesktopRouteType, type NocoBaseDesktopRoute } from '../../../../flow-compat';
15
16
  import type { LayoutDefinition } from '../../../../layout-manager/types';
16
17
  import { AdminLayoutComponent } from '../AdminLayoutComponent';
17
18
  import { AdminLayoutModel } from '../AdminLayoutModel';
19
+ import { AdminLayoutMenuItemModel } from '../AdminLayoutMenuModels';
18
20
  import { AdminLayoutContent } from '../AdminLayoutSlotModels';
19
21
 
22
+ const proLayoutLifecycle = vi.hoisted(() => ({
23
+ events: [] as string[],
24
+ routes: [] as Array<{ children?: Array<{ name?: React.ReactNode }> }>,
25
+ }));
26
+
20
27
  vi.mock('@ant-design/pro-layout', async () => {
21
28
  const ReactModule = await import('react');
22
29
  const RouteContext = ReactModule.createContext({ isMobile: false });
30
+ const ProLayoutMock = (props: {
31
+ children?: React.ReactNode;
32
+ route?: { children?: Array<{ name?: React.ReactNode }> };
33
+ }) => {
34
+ ReactModule.useEffect(() => {
35
+ proLayoutLifecycle.events.push('pro-layout:mount');
36
+ return () => {
37
+ proLayoutLifecycle.events.push('pro-layout:unmount');
38
+ };
39
+ }, []);
40
+ proLayoutLifecycle.routes.push(props.route || {});
23
41
 
24
- return {
25
- default: (props: { children?: React.ReactNode }) =>
42
+ return ReactModule.createElement(
43
+ RouteContext.Provider,
44
+ { value: { isMobile: false } },
26
45
  ReactModule.createElement(
27
- RouteContext.Provider,
28
- { value: { isMobile: false } },
29
- ReactModule.createElement('div', { 'data-testid': 'pro-layout' }, props.children),
46
+ 'div',
47
+ { 'data-testid': 'pro-layout' },
48
+ ReactModule.createElement(
49
+ 'nav',
50
+ { 'data-testid': 'pro-layout-menu' },
51
+ (props.route?.children || []).map((item) =>
52
+ ReactModule.createElement(
53
+ 'span',
54
+ { key: String(item.name), 'data-testid': 'pro-layout-menu-item' },
55
+ item.name,
56
+ ),
57
+ ),
58
+ ),
59
+ props.children,
30
60
  ),
61
+ );
62
+ };
63
+
64
+ return {
65
+ default: ProLayoutMock,
31
66
  RouteContext,
32
67
  };
33
68
  });
@@ -53,6 +88,10 @@ vi.mock('../AppListRender', () => ({
53
88
  useAppListRender: () => undefined,
54
89
  }));
55
90
 
91
+ afterEach(() => {
92
+ delete (window as Window & { __nocobase_modern_client_prefix__?: string }).__nocobase_modern_client_prefix__;
93
+ });
94
+
56
95
  describe('AdminLayoutComponent', () => {
57
96
  it('keeps custom admin layout pages alive by layout route name', async () => {
58
97
  const layout: LayoutDefinition = {
@@ -185,4 +224,212 @@ describe('AdminLayoutComponent', () => {
185
224
 
186
225
  expect(deactivateLayout).toHaveBeenCalledTimes(1);
187
226
  });
227
+
228
+ it('keeps layout content mounted when the menu route tree refreshes', async () => {
229
+ proLayoutLifecycle.events = [];
230
+ proLayoutLifecycle.routes = [];
231
+ const layout: LayoutDefinition = {
232
+ routeName: 'admin',
233
+ routePath: '/admin',
234
+ rootRouteName: 'admin',
235
+ uid: 'admin-layout-model',
236
+ layoutModelClass: 'AdminLayoutModel',
237
+ rootPageModelClass: 'RootPageModel',
238
+ childPageModelClass: 'ChildPageModel',
239
+ authCheck: true,
240
+ };
241
+ const events: string[] = [];
242
+ const engine = new FlowEngine();
243
+ engine.context.defineProperty('routeRepository', {
244
+ value: {
245
+ activateLayout: vi.fn(() => vi.fn()),
246
+ ensureAccessibleLoaded: vi.fn(async () => []),
247
+ listAccessible: () => [],
248
+ subscribe: vi.fn(),
249
+ unsubscribe: vi.fn(),
250
+ },
251
+ });
252
+ engine.context.defineProperty('t', {
253
+ value: (key: string) => key,
254
+ });
255
+ const model = engine.createModel<AdminLayoutModel>({
256
+ uid: layout.uid,
257
+ use: AdminLayoutModel,
258
+ props: {
259
+ layout,
260
+ },
261
+ });
262
+
263
+ const Page = () => {
264
+ useEffect(() => {
265
+ events.push('page:mount');
266
+ return () => {
267
+ events.push('page:unmount');
268
+ };
269
+ }, []);
270
+
271
+ return <div data-testid="custom-admin-route">Workflow tasks</div>;
272
+ };
273
+
274
+ const router = createMemoryRouter(
275
+ [
276
+ {
277
+ id: layout.routeName,
278
+ path: layout.routePath,
279
+ element: <AdminLayoutComponent model={model} />,
280
+ children: [
281
+ {
282
+ id: 'admin.workflow.tasks',
283
+ path: 'workflow/tasks/:taskType?/:status?/:popupId?',
284
+ element: <Page />,
285
+ },
286
+ ],
287
+ },
288
+ ],
289
+ {
290
+ initialEntries: ['/admin/workflow/tasks/approval-apply/pending'],
291
+ },
292
+ );
293
+
294
+ render(
295
+ <FlowEngineProvider engine={engine}>
296
+ <RouterProvider router={router} />
297
+ </FlowEngineProvider>,
298
+ );
299
+
300
+ expect(await screen.findByTestId('custom-admin-route')).toBeInTheDocument();
301
+ expect(proLayoutLifecycle.events).toEqual(['pro-layout:mount']);
302
+
303
+ act(() => {
304
+ model.refreshMenuRouteTree();
305
+ });
306
+
307
+ await act(async () => {
308
+ await router.navigate('/admin/workflow/tasks/approval-apply/pending/1');
309
+ });
310
+
311
+ expect(screen.getByTestId('custom-admin-route')).toBeInTheDocument();
312
+ expect(proLayoutLifecycle.events).toEqual(['pro-layout:mount']);
313
+ expect(events).toEqual(['page:mount']);
314
+ });
315
+
316
+ it('updates ProLayout menu routes after accessible routes change without remounting the layout', async () => {
317
+ proLayoutLifecycle.events = [];
318
+ proLayoutLifecycle.routes = [];
319
+ (window as Window & { __nocobase_modern_client_prefix__?: string }).__nocobase_modern_client_prefix__ = 'v2';
320
+ const layout: LayoutDefinition = {
321
+ routeName: 'admin',
322
+ routePath: '/admin',
323
+ rootRouteName: 'admin',
324
+ uid: 'admin-layout-model',
325
+ layoutModelClass: 'AdminLayoutModel',
326
+ rootPageModelClass: 'RootPageModel',
327
+ childPageModelClass: 'ChildPageModel',
328
+ authCheck: true,
329
+ };
330
+ let accessibleRoutes: NocoBaseDesktopRoute[] = [
331
+ {
332
+ id: 1,
333
+ title: 'Visible page',
334
+ schemaUid: 'visible-page',
335
+ type: NocoBaseDesktopRouteType.flowPage,
336
+ },
337
+ ];
338
+ let routeRepositorySubscriber: (() => void) | undefined;
339
+ const events: string[] = [];
340
+ const engine = new FlowEngine();
341
+ engine.registerModels({
342
+ AdminLayoutMenuItemModel,
343
+ });
344
+ engine.context.defineProperty('routeRepository', {
345
+ value: {
346
+ activateLayout: vi.fn(() => vi.fn()),
347
+ ensureAccessibleLoaded: vi.fn(async () => accessibleRoutes),
348
+ listAccessible: () => accessibleRoutes,
349
+ subscribe: vi.fn((subscriber: () => void) => {
350
+ routeRepositorySubscriber = subscriber;
351
+ }),
352
+ unsubscribe: vi.fn(),
353
+ },
354
+ });
355
+ engine.context.defineProperty('app', {
356
+ value: {
357
+ getPublicPath: () => '/v/',
358
+ router: {
359
+ getBasename: () => '/v',
360
+ },
361
+ },
362
+ });
363
+ engine.context.defineProperty('t', {
364
+ value: (key: string) => key,
365
+ });
366
+ const model = engine.createModel<AdminLayoutModel>({
367
+ uid: layout.uid,
368
+ use: AdminLayoutModel,
369
+ props: {
370
+ layout,
371
+ },
372
+ });
373
+
374
+ const Page = () => {
375
+ useEffect(() => {
376
+ events.push('page:mount');
377
+ return () => {
378
+ events.push('page:unmount');
379
+ };
380
+ }, []);
381
+
382
+ return <div data-testid="custom-admin-route">Workflow tasks</div>;
383
+ };
384
+
385
+ const router = createMemoryRouter(
386
+ [
387
+ {
388
+ id: layout.routeName,
389
+ path: layout.routePath,
390
+ element: <AdminLayoutComponent model={model} />,
391
+ children: [
392
+ {
393
+ id: 'admin.workflow.tasks',
394
+ path: 'workflow/tasks/:taskType?/:status?/:popupId?',
395
+ element: <Page />,
396
+ },
397
+ ],
398
+ },
399
+ ],
400
+ {
401
+ initialEntries: ['/admin/workflow/tasks/approval-apply/pending'],
402
+ },
403
+ );
404
+
405
+ render(
406
+ <FlowEngineProvider engine={engine}>
407
+ <RouterProvider router={router} />
408
+ </FlowEngineProvider>,
409
+ );
410
+
411
+ expect(await screen.findByText('Visible page')).toBeInTheDocument();
412
+ expect(proLayoutLifecycle.events).toEqual(['pro-layout:mount']);
413
+
414
+ act(() => {
415
+ accessibleRoutes = [];
416
+ routeRepositorySubscriber?.();
417
+ });
418
+
419
+ await waitFor(() => {
420
+ expect(screen.queryByText('Visible page')).not.toBeInTheDocument();
421
+ });
422
+
423
+ await act(async () => {
424
+ await router.navigate('/admin/workflow/tasks/approval-apply/pending/1');
425
+ });
426
+
427
+ expect(screen.getByTestId('custom-admin-route')).toBeInTheDocument();
428
+ expect(proLayoutLifecycle.events).toEqual(['pro-layout:mount']);
429
+ expect(events).toEqual(['page:mount']);
430
+ expect(
431
+ proLayoutLifecycle.routes.some((route) => route.children?.some((item) => item.name === 'Visible page')),
432
+ ).toBe(true);
433
+ expect(proLayoutLifecycle.routes.at(-1)?.children?.some((item) => item.name === 'Visible page')).toBe(false);
434
+ });
188
435
  });
@@ -26,6 +26,10 @@ export default function useStyle() {
26
26
  () => ({
27
27
  [`.${COMPONENT_CLS}`]: {
28
28
  '.vditor-reset': { fontSize: `${token.fontSize}px !important`, color: 'unset' },
29
+ '.vditor-reset h2': {
30
+ borderBottom: 'none !important',
31
+ boxShadow: 'none !important',
32
+ },
29
33
  '.vditor': {
30
34
  borderRadius: 8,
31
35
  },
@@ -150,7 +150,7 @@ export const BlockItemCard = React.forwardRef(
150
150
  ) => {
151
151
  const { t } = useTranslation();
152
152
  const { token } = theme.useToken();
153
- const { title: blockTitle, description, children, className, heightMode, ...rest } = props;
153
+ const { title: blockTitle, description, children, className, heightMode, style, ...rest } = props;
154
154
  const cardRef = useRef<HTMLDivElement | null>(null);
155
155
  const setCardRef = useCallback(
156
156
  (node: HTMLDivElement | null) => {
@@ -185,7 +185,7 @@ export const BlockItemCard = React.forwardRef(
185
185
  <Card
186
186
  ref={setCardRef as any}
187
187
  title={title}
188
- style={{ display: 'flex', flexDirection: 'column', height: height }}
188
+ style={{ display: 'flex', flexDirection: 'column', ...style, height: height ?? style?.height }}
189
189
  styles={{
190
190
  body: { flex: 1, display: 'flex', flexDirection: 'column', overflow: 'auto' },
191
191
  header: {
@@ -7,7 +7,7 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import React, { useMemo, useState } from 'react';
10
+ import React, { useCallback, useMemo, useState } from 'react';
11
11
  import { DatePicker, Select, Space } from 'antd';
12
12
  import { inferPickerType } from '../../flow-compat';
13
13
  import { dayjs, getDateTimeFormat, getPickerFormat } from '@nocobase/utils/client';
@@ -29,6 +29,7 @@ export interface FieldAssignExactDatePickerProps {
29
29
  isRange?: boolean;
30
30
  onChange?: (value: ExactDatePickerValue) => void;
31
31
  style?: React.CSSProperties;
32
+ disabled?: boolean;
32
33
  }
33
34
 
34
35
  function getRawSingleValue(value: unknown, isRange: boolean): unknown {
@@ -114,7 +115,7 @@ function parseRangeValue(value: unknown, format: string): [dayjs.Dayjs, dayjs.Da
114
115
  }
115
116
 
116
117
  export const FieldAssignExactDatePicker: React.FC<FieldAssignExactDatePickerProps> = (props) => {
117
- const { picker = 'date', format, showTime, timeFormat, value, isRange = false, onChange, style } = props;
118
+ const { picker = 'date', format, showTime, timeFormat, value, isRange = false, onChange, style, disabled } = props;
118
119
  const flowCtx = useFlowContext();
119
120
  const t = flowCtx.model.translate.bind(flowCtx.model);
120
121
 
@@ -124,17 +125,17 @@ export const FieldAssignExactDatePicker: React.FC<FieldAssignExactDatePickerProp
124
125
  return inferPickerFromRawValue(rawSingleValue, picker);
125
126
  });
126
127
 
127
- const getResolvedFormat = (nextPicker: ExactDatePickerMode) => {
128
- const baseFormat = nextPicker === picker && format ? format : getPickerFormat(nextPicker);
129
- const dateFormat = nextPicker === 'date' ? stripTimeFromFormat(baseFormat) : baseFormat;
130
- return getDateTimeFormat(nextPicker, dateFormat, showTime, timeFormat);
131
- };
132
-
133
- const resolvedFormat = useMemo(
134
- () => getResolvedFormat(targetPicker),
135
- [targetPicker, format, picker, showTime, timeFormat],
128
+ const getResolvedFormat = useCallback(
129
+ (nextPicker: ExactDatePickerMode) => {
130
+ const baseFormat = nextPicker === picker && format ? format : getPickerFormat(nextPicker);
131
+ const dateFormat = nextPicker === 'date' ? stripTimeFromFormat(baseFormat) : baseFormat;
132
+ return getDateTimeFormat(nextPicker, dateFormat, showTime, timeFormat);
133
+ },
134
+ [format, picker, showTime, timeFormat],
136
135
  );
137
136
 
137
+ const resolvedFormat = useMemo(() => getResolvedFormat(targetPicker), [getResolvedFormat, targetPicker]);
138
+
138
139
  const singleValue = useMemo(() => {
139
140
  if (isRange) return null;
140
141
  return parseDateFromRawValue(value, resolvedFormat);
@@ -156,6 +157,10 @@ export const FieldAssignExactDatePicker: React.FC<FieldAssignExactDatePickerProp
156
157
  );
157
158
 
158
159
  const handlePickerTypeChange = (nextPicker: ExactDatePickerMode) => {
160
+ if (disabled) {
161
+ return;
162
+ }
163
+
159
164
  setTargetPicker(nextPicker);
160
165
 
161
166
  if (!onChange) {
@@ -182,7 +187,11 @@ export const FieldAssignExactDatePicker: React.FC<FieldAssignExactDatePickerProp
182
187
  inputReadOnly: flowCtx.isMobileLayout,
183
188
  showTime: showTime ? { defaultValue: dayjs('00:00:00', 'HH:mm:ss') } : false,
184
189
  value: singleValue,
190
+ disabled,
185
191
  onChange: (nextDate: dayjs.Dayjs | null) => {
192
+ if (disabled) {
193
+ return;
194
+ }
186
195
  if (nextDate && dayjs.isDayjs(nextDate)) {
187
196
  onChange?.(nextDate);
188
197
  return;
@@ -200,7 +209,11 @@ export const FieldAssignExactDatePicker: React.FC<FieldAssignExactDatePickerProp
200
209
  inputReadOnly: flowCtx.isMobileLayout,
201
210
  showTime: showTime ? { defaultValue: [dayjs('00:00:00', 'HH:mm:ss'), dayjs('23:59:59', 'HH:mm:ss')] } : false,
202
211
  value: rangeValue,
212
+ disabled,
203
213
  onChange: (nextDates: [dayjs.Dayjs | null, dayjs.Dayjs | null] | null) => {
214
+ if (disabled) {
215
+ return;
216
+ }
204
217
  if (Array.isArray(nextDates) && nextDates[0] && nextDates[1]) {
205
218
  onChange?.([nextDates[0], nextDates[1]]);
206
219
  return;
@@ -218,6 +231,7 @@ export const FieldAssignExactDatePicker: React.FC<FieldAssignExactDatePickerProp
218
231
  value={targetPicker}
219
232
  options={pickerOptions}
220
233
  onChange={handlePickerTypeChange}
234
+ disabled={disabled}
221
235
  />
222
236
  {isRange ? <DatePicker.RangePicker {...rangePickerProps} /> : <DatePicker {...singlePickerProps} />}
223
237
  </Space.Compact>