@nocobase/client-v2 2.1.0-beta.36 → 2.1.0-beta.38

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 (154) hide show
  1. package/es/Application.d.ts +1 -0
  2. package/es/BaseApplication.d.ts +4 -0
  3. package/es/RouterManager.d.ts +1 -0
  4. package/es/components/KeepAlive.d.ts +22 -0
  5. package/es/components/RouterBridge.d.ts +9 -0
  6. package/es/components/form/DialogFormLayout.d.ts +5 -29
  7. package/es/components/form/filter/CollectionFilter.d.ts +41 -0
  8. package/es/components/form/filter/CollectionFilterItem.d.ts +41 -0
  9. package/es/components/form/filter/DateFilterDynamicComponent.d.ts +57 -0
  10. package/es/components/form/filter/FilterValueInput.d.ts +29 -0
  11. package/es/components/form/filter/index.d.ts +11 -0
  12. package/es/components/form/filter/useFilterActionProps.d.ts +96 -0
  13. package/es/components/form/index.d.ts +1 -0
  14. package/es/data-source/ExtendCollectionsProvider.d.ts +50 -0
  15. package/es/data-source/index.d.ts +9 -0
  16. package/es/flow/FlowPage.d.ts +2 -1
  17. package/es/flow/admin-shell/AdminLayoutRouteCoordinator.d.ts +8 -40
  18. package/es/flow/admin-shell/BaseLayoutModel.d.ts +89 -0
  19. package/es/flow/admin-shell/BaseLayoutRouteCoordinator.d.ts +74 -0
  20. package/es/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.d.ts +12 -0
  21. package/es/flow/admin-shell/admin-layout/AdminLayoutModel.d.ts +7 -92
  22. package/es/flow/admin-shell/admin-layout/index.d.ts +2 -0
  23. package/es/flow/admin-shell/useAdminLayoutRoutePage.d.ts +2 -2
  24. package/es/flow/admin-shell/useLayoutRoutePage.d.ts +23 -0
  25. package/es/flow/components/FlowRoute.d.ts +10 -1
  26. package/es/flow/components/filter/index.d.ts +2 -0
  27. package/es/flow/components/filter/useFilterOptions.d.ts +54 -0
  28. package/es/flow/index.d.ts +4 -0
  29. package/es/flow/models/base/PageModel/PageModel.d.ts +3 -1
  30. package/es/flow/models/blocks/form/FormActionGroupModel.d.ts +1 -0
  31. package/es/flow/models/blocks/table/TableBlockModel.d.ts +10 -0
  32. package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.d.ts +1 -1
  33. package/es/flow-compat/passwordUtils.d.ts +1 -1
  34. package/es/index.d.ts +2 -0
  35. package/es/index.mjs +491 -439
  36. package/es/layout-manager/LayoutContentRoute.d.ts +14 -0
  37. package/es/layout-manager/LayoutManager.d.ts +22 -0
  38. package/es/layout-manager/LayoutRoute.d.ts +14 -0
  39. package/es/layout-manager/index.d.ts +13 -0
  40. package/es/layout-manager/types.d.ts +20 -0
  41. package/es/layout-manager/utils.d.ts +14 -0
  42. package/es/nocobase-buildin-plugin/index.d.ts +3 -10
  43. package/es/settings-center/index.d.ts +1 -1
  44. package/es/settings-center/plugin-manager/BulkEnableButton.d.ts +15 -0
  45. package/es/settings-center/plugin-manager/PluginCard.d.ts +15 -0
  46. package/es/settings-center/plugin-manager/PluginDetail.d.ts +16 -0
  47. package/es/settings-center/{PluginManagerPage.d.ts → plugin-manager/index.d.ts} +1 -7
  48. package/es/settings-center/plugin-manager/types.d.ts +34 -0
  49. package/lib/index.js +491 -439
  50. package/package.json +8 -7
  51. package/src/Application.tsx +27 -12
  52. package/src/BaseApplication.tsx +19 -0
  53. package/src/PluginSettingsManager.ts +1 -1
  54. package/src/RouterManager.tsx +17 -1
  55. package/src/__tests__/PluginSettingsManager.test.ts +41 -2
  56. package/src/__tests__/app.test.tsx +17 -1
  57. package/src/__tests__/globalDeps.test.ts +1 -0
  58. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +45 -2
  59. package/src/__tests__/plugin-manager.test.tsx +177 -0
  60. package/src/__tests__/settings-center.test.tsx +24 -2
  61. package/src/components/KeepAlive.tsx +131 -0
  62. package/src/components/README.md +89 -6
  63. package/src/components/README.zh-CN.md +89 -7
  64. package/src/components/RouterBridge.tsx +28 -4
  65. package/src/components/__tests__/KeepAlive.test.tsx +63 -0
  66. package/src/components/__tests__/RouterBridge.test.tsx +27 -0
  67. package/src/components/form/DialogFormLayout.tsx +5 -29
  68. package/src/components/form/filter/CollectionFilter.tsx +101 -0
  69. package/src/components/form/filter/CollectionFilterItem.tsx +176 -0
  70. package/src/components/form/filter/DateFilterDynamicComponent.tsx +283 -0
  71. package/src/components/form/filter/FilterValueInput.tsx +198 -0
  72. package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +205 -0
  73. package/src/components/form/filter/__tests__/DateFilterDynamicComponent.test.tsx +148 -0
  74. package/src/components/form/filter/__tests__/FilterValueInput.test.tsx +243 -0
  75. package/src/components/form/filter/__tests__/compileFilterGroup.test.ts +146 -0
  76. package/src/components/form/filter/index.ts +13 -0
  77. package/src/components/form/filter/useFilterActionProps.ts +200 -0
  78. package/src/components/form/index.tsx +1 -0
  79. package/src/data-source/ExtendCollectionsProvider.tsx +144 -0
  80. package/src/data-source/__tests__/ExtendCollectionsProvider.test.tsx +264 -0
  81. package/src/data-source/index.ts +10 -0
  82. package/src/flow/FlowPage.tsx +35 -7
  83. package/src/flow/__tests__/FlowPage.test.tsx +79 -0
  84. package/src/flow/__tests__/FlowRoute.test.tsx +529 -2
  85. package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +191 -0
  86. package/src/flow/actions/__tests__/openView.subModelKey.test.tsx +33 -0
  87. package/src/flow/actions/aclCheck.tsx +4 -0
  88. package/src/flow/actions/aclCheckRefresh.tsx +4 -0
  89. package/src/flow/actions/dateTimeFormat.tsx +12 -8
  90. package/src/flow/actions/linkageRules.tsx +122 -0
  91. package/src/flow/actions/openView.tsx +28 -4
  92. package/src/flow/admin-shell/AdminLayoutRouteCoordinator.ts +11 -329
  93. package/src/flow/admin-shell/BaseLayoutModel.tsx +455 -0
  94. package/src/flow/admin-shell/BaseLayoutRouteCoordinator.ts +502 -0
  95. package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +547 -3
  96. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +4 -4
  97. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +160 -0
  98. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +0 -12
  99. package/src/flow/admin-shell/admin-layout/AdminLayoutModel.tsx +28 -201
  100. package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +11 -2
  101. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +1 -26
  102. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +149 -27
  103. package/src/flow/admin-shell/admin-layout/index.ts +2 -0
  104. package/src/flow/admin-shell/useAdminLayoutRoutePage.ts +10 -26
  105. package/src/flow/admin-shell/useLayoutRoutePage.ts +61 -0
  106. package/src/flow/components/AdminLayout.tsx +4 -154
  107. package/src/flow/components/FlowRoute.tsx +105 -15
  108. package/src/flow/components/filter/index.ts +3 -0
  109. package/src/flow/components/filter/useFilterOptions.ts +80 -0
  110. package/src/flow/index.ts +4 -0
  111. package/src/flow/models/base/ActionModel.tsx +8 -1
  112. package/src/flow/models/base/PageModel/PageModel.tsx +51 -18
  113. package/src/flow/models/base/PageModel/RootPageModel.tsx +6 -13
  114. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +102 -1
  115. package/src/flow/models/base/RouteModel.tsx +1 -1
  116. package/src/flow/models/blocks/form/FormActionGroupModel.tsx +14 -0
  117. package/src/flow/models/blocks/form/FormItemModel.tsx +8 -1
  118. package/src/flow/models/blocks/form/__tests__/FormActionGroupModel.test.ts +46 -0
  119. package/src/flow/models/blocks/form/submitValues.ts +4 -1
  120. package/src/flow/models/blocks/table/TableBlockModel.tsx +118 -16
  121. package/src/flow/models/blocks/table/__tests__/TableBlockModel.rowSelection.test.tsx +114 -0
  122. package/src/flow/models/fields/AssociationFieldModel/SubFormFieldModel.tsx +7 -1
  123. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +1 -1
  124. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +6 -5
  125. package/src/flow/models/fields/ClickableFieldModel.tsx +9 -1
  126. package/src/flow/models/fields/DisplayTimeFieldModel.tsx +1 -1
  127. package/src/flow/models/fields/TimeFieldModel.tsx +1 -1
  128. package/src/flow/models/fields/__tests__/TimeFieldModel.test.tsx +61 -0
  129. package/src/flow/models/fields/mobile-components/MobileDatePicker.tsx +19 -3
  130. package/src/flow/models/fields/mobile-components/__tests__/MobileDatePicker.test.tsx +94 -0
  131. package/src/flow/models/topbar/TopbarActionModel.tsx +1 -1
  132. package/src/flow/utils/__tests__/dateTimeFormat.test.ts +91 -0
  133. package/src/index.ts +2 -0
  134. package/src/layout-manager/LayoutContentRoute.tsx +90 -0
  135. package/src/layout-manager/LayoutManager.tsx +185 -0
  136. package/src/layout-manager/LayoutRoute.tsx +138 -0
  137. package/src/layout-manager/__tests__/LayoutManager.test.tsx +335 -0
  138. package/src/layout-manager/__tests__/LayoutRoute.test.tsx +473 -0
  139. package/src/layout-manager/index.ts +14 -0
  140. package/src/layout-manager/types.ts +22 -0
  141. package/src/layout-manager/utils.ts +37 -0
  142. package/src/nocobase-buildin-plugin/index.tsx +69 -67
  143. package/src/nocobase-buildin-plugin/plugins/LocalePlugin.ts +1 -0
  144. package/src/settings-center/index.ts +1 -1
  145. package/src/settings-center/plugin-manager/BulkEnableButton.tsx +111 -0
  146. package/src/settings-center/plugin-manager/PluginCard.tsx +270 -0
  147. package/src/settings-center/plugin-manager/PluginDetail.tsx +195 -0
  148. package/src/settings-center/plugin-manager/index.tsx +254 -0
  149. package/src/settings-center/plugin-manager/types.ts +35 -0
  150. package/src/settings-center/utils.tsx +8 -1
  151. package/src/theme/__tests__/globalStyles.test.ts +24 -0
  152. package/src/theme/globalStyles.ts +10 -0
  153. package/src/utils/globalDeps.ts +2 -0
  154. package/src/settings-center/PluginManagerPage.tsx +0 -162
@@ -0,0 +1,46 @@
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 { FlowEngine } from '@nocobase/flow-engine';
11
+ import { describe, expect, it } from 'vitest';
12
+ import { FormActionGroupModel, FormActionModel } from '../../../..';
13
+
14
+ describe('FormActionGroupModel', () => {
15
+ it('filters addable form actions by allowedFormActionModelNames', async () => {
16
+ class AllowedFormActionModel extends FormActionModel {}
17
+ class HiddenFormActionModel extends FormActionModel {}
18
+
19
+ AllowedFormActionModel.define({
20
+ label: 'Allowed action',
21
+ sort: 10,
22
+ });
23
+ HiddenFormActionModel.define({
24
+ label: 'Hidden action',
25
+ sort: 20,
26
+ });
27
+
28
+ const engine = new FlowEngine();
29
+ engine.registerModels({
30
+ FormActionModel,
31
+ HiddenFormActionModel,
32
+ });
33
+ engine.registerModelLoaders({
34
+ AllowedFormActionModel: {
35
+ loader: async () => ({ AllowedFormActionModel }),
36
+ },
37
+ });
38
+
39
+ const items = await FormActionGroupModel.defineChildren({
40
+ engine,
41
+ allowedFormActionModelNames: ['AllowedFormActionModel'],
42
+ } as any);
43
+
44
+ expect(items.map((item: any) => item.useModel)).toEqual(['AllowedFormActionModel']);
45
+ });
46
+ });
@@ -84,7 +84,10 @@ function forEachModelIncludingForks(engine: any, visitor: (model: any) => void)
84
84
 
85
85
  const forks: any = model?.forks;
86
86
  if (forks && typeof forks.forEach === 'function') {
87
- forks.forEach((fork: any) => visitor(fork));
87
+ forks.forEach((fork: any) => {
88
+ if (!fork || fork.disposed) return;
89
+ visitor(fork);
90
+ });
88
91
  }
89
92
  });
90
93
  }
@@ -39,6 +39,7 @@ import { TableColumnModel } from './TableColumnModel';
39
39
  import { extractIndex, adjustColumnOrder, setNestedValue, extractIds, getRowKey, useBlockHeight } from './utils';
40
40
  import { resolveTableSorterField } from './sortUtils';
41
41
  import { commonConditionHandler, ConditionBuilder } from '../../../components/ConditionBuilder';
42
+ import { BulkDeleteActionModel } from '../../actions/BulkDeleteActionModel';
42
43
  import {
43
44
  applyMobilePaginationProps,
44
45
  createCompactSimpleItemRender,
@@ -128,6 +129,18 @@ const rowSelectCheckboxWrapperNoIndexClass = css`
128
129
  }
129
130
  `;
130
131
 
132
+ const leftAuxiliaryColumnCellClass = css`
133
+ position: relative;
134
+ display: flex;
135
+ align-items: center;
136
+ justify-content: center;
137
+ min-height: 22px;
138
+ `;
139
+
140
+ const leftAuxiliaryColumnCellWithHandleClass = css`
141
+ padding-left: 24px;
142
+ `;
143
+
131
144
  const highlightedRowClass = css`
132
145
  & td {
133
146
  background-color: #e6f7ff !important;
@@ -382,21 +395,76 @@ export class TableBlockModel extends CollectionBlockModel<TableBlockModelStructu
382
395
  },
383
396
  };
384
397
 
385
- renderCell = (checked, record, index, originNode) => {
386
- if (!this.props.dragSort && !this.props.showIndex) {
387
- return originNode;
388
- }
389
- const current = this.resource.getPage();
398
+ isRowSelectionEnabled() {
399
+ return this.props.enableRowSelection !== false;
400
+ }
401
+
402
+ isShowIndexEnabled() {
403
+ return this.props.showIndex !== false;
404
+ }
390
405
 
406
+ getRecordIndex(record: Record<string, unknown>, index: number) {
407
+ let nextIndex = index;
408
+ const current = this.resource.getPage();
391
409
  const pageSize = this.resource.getPageSize() || 20;
392
410
  if (current) {
393
- index = index + (current - 1) * pageSize + 1;
411
+ nextIndex = nextIndex + (current - 1) * pageSize + 1;
394
412
  } else {
395
- index = index + 1;
413
+ nextIndex = nextIndex + 1;
396
414
  }
397
- if (record.__index) {
398
- index = extractIndex(record.__index);
415
+ const treeIndex = record?.__index;
416
+ if (treeIndex) {
417
+ return extractIndex(treeIndex);
399
418
  }
419
+ return nextIndex;
420
+ }
421
+
422
+ getLeftAuxiliaryColumn() {
423
+ const showIndex = this.isShowIndexEnabled();
424
+ const showDragHandle = this.props.dragSort && this.props.dragSortBy;
425
+ if (!showIndex && !showDragHandle) {
426
+ return null;
427
+ }
428
+
429
+ return {
430
+ key: '__rowSelectionDisabledAuxiliary__',
431
+ width: showDragHandle ? 74 : 50,
432
+ align: 'center' as const,
433
+ render: (_value: unknown, record: Record<string, unknown>, index: number) => {
434
+ const displayIndex = this.getRecordIndex(record, index);
435
+ const rowKey = getRowKey(record, this.collection.filterTargetKey);
436
+ const rowKeyString = rowKey == null ? rowKey : String(rowKey);
437
+ return (
438
+ <div
439
+ className={classNames(leftAuxiliaryColumnCellClass, {
440
+ [leftAuxiliaryColumnCellWithHandleClass]: showDragHandle,
441
+ })}
442
+ >
443
+ {showDragHandle && (
444
+ <SortHandle
445
+ id={rowKeyString}
446
+ style={{
447
+ position: 'absolute',
448
+ left: 0,
449
+ top: '50%',
450
+ justifyContent: 'center',
451
+ transform: 'translateY(-50%)',
452
+ }}
453
+ />
454
+ )}
455
+ {showIndex && <TableIndex index={displayIndex} aria-label={`table-index-${displayIndex}`} />}
456
+ </div>
457
+ );
458
+ },
459
+ };
460
+ }
461
+
462
+ renderCell = (checked, record, index, originNode) => {
463
+ const showIndex = this.isShowIndexEnabled();
464
+ if (!this.props.dragSort && !showIndex) {
465
+ return originNode;
466
+ }
467
+ index = this.getRecordIndex(record, index);
400
468
  const rowKey = getRowKey(record, this.collection.filterTargetKey);
401
469
  const rowKeyString = rowKey == null ? rowKey : String(rowKey);
402
470
  const showDragHandle = this.props.dragSort && this.props.dragSortBy;
@@ -406,7 +474,7 @@ export class TableBlockModel extends CollectionBlockModel<TableBlockModelStructu
406
474
  aria-label={`table-index-${index}`}
407
475
  className={classNames(checked ? 'checked' : null, rowSelectCheckboxWrapperClass, {
408
476
  [rowSelectCheckboxWrapperClassHover]: true,
409
- [rowSelectCheckboxWrapperNoIndexClass]: !this.props.showIndex,
477
+ [rowSelectCheckboxWrapperNoIndexClass]: !showIndex,
410
478
  })}
411
479
  >
412
480
  {showDragHandle && (
@@ -422,7 +490,7 @@ export class TableBlockModel extends CollectionBlockModel<TableBlockModelStructu
422
490
  />
423
491
  )}
424
492
  <div className={classNames(checked ? 'checked' : null, rowSelectCheckboxContentClass)}>
425
- {this.props.showIndex && <TableIndex index={index} />}
493
+ {showIndex && <TableIndex index={index} />}
426
494
  </div>
427
495
 
428
496
  <div className={classNames('nb-origin-node', checked ? 'checked' : null, rowSelectCheckboxCheckedClassHover)}>
@@ -445,6 +513,16 @@ export class TableBlockModel extends CollectionBlockModel<TableBlockModelStructu
445
513
  );
446
514
  }
447
515
 
516
+ shouldRenderAction(action: ActionModel, isConfigMode: boolean) {
517
+ if (action.hidden && !isConfigMode) {
518
+ return false;
519
+ }
520
+ if (!isConfigMode && !this.isRowSelectionEnabled() && action instanceof BulkDeleteActionModel) {
521
+ return false;
522
+ }
523
+ return true;
524
+ }
525
+
448
526
  pagination() {
449
527
  const totalCount = this.resource.getMeta('count');
450
528
  const pageSize = this.resource.getPageSize();
@@ -509,6 +587,9 @@ export class TableBlockModel extends CollectionBlockModel<TableBlockModelStructu
509
587
  >
510
588
  <Space wrap>
511
589
  {this.mapSubModels('actions', (action) => {
590
+ if (!this.shouldRenderAction(action, isConfigMode)) {
591
+ return null;
592
+ }
512
593
  // @ts-ignore
513
594
  if (action.props.position === 'left') {
514
595
  return (
@@ -527,8 +608,8 @@ export class TableBlockModel extends CollectionBlockModel<TableBlockModelStructu
527
608
  </Space>
528
609
  <Space wrap>
529
610
  {this.mapSubModels('actions', (action) => {
530
- if (action.hidden && !isConfigMode) {
531
- return;
611
+ if (!this.shouldRenderAction(action, isConfigMode)) {
612
+ return null;
532
613
  }
533
614
  // @ts-ignore
534
615
  if (action.props.position !== 'left') {
@@ -673,6 +754,20 @@ TableBlockModel.registerFlow({
673
754
  });
674
755
  },
675
756
  },
757
+ enableRowSelection: {
758
+ title: tExpr('Enable row selection'),
759
+ uiMode: { type: 'switch', key: 'enableRowSelection' },
760
+ defaultParams: {
761
+ enableRowSelection: true,
762
+ },
763
+ handler(ctx, params) {
764
+ const model = ctx.model as TableBlockModel;
765
+ model.setProps('enableRowSelection', params.enableRowSelection);
766
+ if (!params.enableRowSelection) {
767
+ model.resource.setSelectedRows([]);
768
+ }
769
+ },
770
+ },
676
771
  showRowNumbers: {
677
772
  title: tExpr('Show row numbers'),
678
773
  uiMode: { type: 'switch', key: 'showIndex' },
@@ -875,7 +970,11 @@ const HighPerformanceTable = React.memo(
875
970
  [model.collection.filterTargetKey],
876
971
  );
877
972
 
973
+ const enableRowSelection = model.isRowSelectionEnabled();
878
974
  const rowSelection = useMemo(() => {
975
+ if (!enableRowSelection) {
976
+ return undefined;
977
+ }
879
978
  return {
880
979
  columnWidth: 50,
881
980
  type: 'checkbox',
@@ -887,7 +986,10 @@ const HighPerformanceTable = React.memo(
887
986
  renderCell: model.renderCell,
888
987
  ...model.rowSelectionProps,
889
988
  };
890
- }, [model, selectedRowKeys]);
989
+ }, [enableRowSelection, model, selectedRowKeys]);
990
+
991
+ const leftAuxiliaryColumn = enableRowSelection ? null : model.getLeftAuxiliaryColumn();
992
+ const mergedColumns = leftAuxiliaryColumn ? [leftAuxiliaryColumn, ...columns] : columns;
891
993
 
892
994
  const handleChange = useCallback(
893
995
  async (pagination, filters, sorter) => {
@@ -923,7 +1025,7 @@ const HighPerformanceTable = React.memo(
923
1025
  [highlightedRowKey, model.collection?.filterTargetKey],
924
1026
  );
925
1027
 
926
- const pagination = useMemo(() => _pagination, [dataSource]);
1028
+ const pagination = _pagination;
927
1029
 
928
1030
  const onRow = useCallback(
929
1031
  (record, rowIndex) => {
@@ -1029,7 +1131,7 @@ const HighPerformanceTable = React.memo(
1029
1131
  virtual={virtual}
1030
1132
  scroll={tableScroll}
1031
1133
  dataSource={dataSource}
1032
- columns={columns}
1134
+ columns={mergedColumns}
1033
1135
  pagination={pagination}
1034
1136
  onChange={handleChange}
1035
1137
  rowClassName={rowClassName}
@@ -0,0 +1,114 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ import { render, screen } from '@testing-library/react';
11
+ import { FlowEngine } from '@nocobase/flow-engine';
12
+ import '@nocobase/client';
13
+ import React from 'react';
14
+ import { describe, expect, it } from 'vitest';
15
+ import { BulkDeleteActionModel } from '../../../actions/BulkDeleteActionModel';
16
+ import { TableBlockModel } from '../TableBlockModel';
17
+
18
+ function createTableModel() {
19
+ const engine = new FlowEngine();
20
+ engine.registerModels({ TableBlockModel });
21
+
22
+ const ds = engine.dataSourceManager.getDataSource('main');
23
+ ds.addCollection({
24
+ name: 'posts',
25
+ filterTargetKey: 'id',
26
+ fields: [
27
+ { name: 'id', type: 'integer', interface: 'number' },
28
+ { name: 'title', type: 'string', interface: 'input' },
29
+ ],
30
+ });
31
+
32
+ return engine.createModel<TableBlockModel>({
33
+ uid: 'posts-table',
34
+ use: 'TableBlockModel',
35
+ stepParams: {
36
+ resourceSettings: {
37
+ init: {
38
+ dataSourceKey: 'main',
39
+ collectionName: 'posts',
40
+ },
41
+ },
42
+ },
43
+ });
44
+ }
45
+
46
+ describe('TableBlockModel row selection setting', () => {
47
+ it('keeps row selection enabled by default', () => {
48
+ const model = createTableModel();
49
+
50
+ expect(model.isRowSelectionEnabled()).toBe(true);
51
+
52
+ model.setProps('enableRowSelection', false);
53
+
54
+ expect(model.isRowSelectionEnabled()).toBe(false);
55
+ });
56
+
57
+ it('builds a standalone row-number column when row selection is disabled', () => {
58
+ const model = createTableModel();
59
+ model.resource.setPage(2);
60
+ model.resource.setPageSize(20);
61
+ model.setProps('enableRowSelection', false);
62
+ model.setProps('showIndex', true);
63
+
64
+ const column = model.getLeftAuxiliaryColumn();
65
+
66
+ expect(column).toMatchObject({
67
+ key: '__rowSelectionDisabledAuxiliary__',
68
+ width: 50,
69
+ align: 'center',
70
+ });
71
+
72
+ render(<>{column?.render(null, { id: 1, title: 'first post' }, 1)}</>);
73
+
74
+ expect(screen.getByLabelText('table-index-22').textContent).toBe('22');
75
+ });
76
+
77
+ it('omits the standalone column when row selection and row numbers are both disabled', () => {
78
+ const model = createTableModel();
79
+ model.setProps('enableRowSelection', false);
80
+ model.setProps('showIndex', false);
81
+
82
+ expect(model.getLeftAuxiliaryColumn()).toBeNull();
83
+ });
84
+
85
+ it('keeps a standalone drag handle column without row selection checkboxes', () => {
86
+ const model = createTableModel();
87
+ model.setProps('enableRowSelection', false);
88
+ model.setProps('showIndex', false);
89
+ model.setProps('dragSort', true);
90
+ model.setProps('dragSortBy', 'sort');
91
+
92
+ expect(model.getLeftAuxiliaryColumn()).toMatchObject({
93
+ key: '__rowSelectionDisabledAuxiliary__',
94
+ width: 74,
95
+ align: 'center',
96
+ });
97
+ });
98
+
99
+ it('hides bulk delete in runtime when row selection is disabled', () => {
100
+ const model = createTableModel();
101
+ const bulkDelete = new BulkDeleteActionModel({
102
+ uid: 'bulk-delete',
103
+ use: 'BulkDeleteActionModel',
104
+ flowEngine: model.flowEngine,
105
+ });
106
+
107
+ expect(model.shouldRenderAction(bulkDelete, false)).toBe(true);
108
+
109
+ model.setProps('enableRowSelection', false);
110
+
111
+ expect(model.shouldRenderAction(bulkDelete, false)).toBe(false);
112
+ expect(model.shouldRenderAction(bulkDelete, true)).toBe(true);
113
+ });
114
+ });
@@ -213,6 +213,7 @@ const ArrayNester = ({
213
213
  const isConfigMode = !!model.context.flowSettingsEnabled;
214
214
  const { t } = useTranslation();
215
215
  const rowIndex = model.context.fieldIndex || [];
216
+ const parentFieldPathArray = (model?.parent as any)?.context?.fieldPathArray || [];
216
217
  // 用来缓存每行的 fork,保证每行只创建一次
217
218
  const forksRef = useRef<Record<string, any>>({});
218
219
  const collectionName = model.context.collectionField.name;
@@ -251,7 +252,7 @@ const ArrayNester = ({
251
252
  {displayFields.map((field: any, index) => {
252
253
  const { key, name: fieldName, isDefault } = field;
253
254
  const fieldIndex = [...rowIndex, `${collectionName}:${index}`];
254
-
255
+ const fieldPathArray = parentFieldPathArray;
255
256
  // 每行只创建一次 fork
256
257
  if (!forksRef.current[key]) {
257
258
  const fork = gridModel.createFork({ disabled });
@@ -268,6 +269,11 @@ const ArrayNester = ({
268
269
  get: () => fieldIndex,
269
270
  cache: false,
270
271
  });
272
+ console.log(fieldPathArray);
273
+ currentFork.context.defineProperty('fieldPathArray', {
274
+ get: () => fieldPathArray,
275
+ cache: false,
276
+ });
271
277
 
272
278
  const getRowItem = createItemChainGetter({
273
279
  valueAccessor: () => currentFork.context.form.getFieldValue([name, fieldName]),
@@ -269,7 +269,7 @@ export function SubTableField(props) {
269
269
  display: 'flex',
270
270
  justifyContent: 'space-between',
271
271
  alignItems: 'center',
272
- minHeight: '20px',
272
+ minHeight: '36px',
273
273
  }}
274
274
  >
275
275
  <Space>
@@ -60,13 +60,13 @@ export class SubTableFieldModel extends AssociationFieldModel {
60
60
  setCurrentPage;
61
61
  currentPageSize;
62
62
 
63
- getCurrentValue = () => {
63
+ getCurrentValue() {
64
64
  const fallback = Array.isArray(this.props.value) ? this.props.value : [];
65
- const fieldPathArray = this.parent?.context?.fieldPathArray;
65
+ const fieldPathArray = this.context.fieldPathArray ?? this.parent?.context?.fieldPathArray;
66
66
  if (!Array.isArray(fieldPathArray) || !fieldPathArray.length) return fallback;
67
67
  const latest = this.context.blockModel?.context?.form?.getFieldValue?.(fieldPathArray as any);
68
68
  return Array.isArray(latest) ? latest : fallback;
69
- };
69
+ }
70
70
 
71
71
  get collection() {
72
72
  return this.context.collection;
@@ -111,6 +111,7 @@ export class SubTableFieldModel extends AssociationFieldModel {
111
111
  },
112
112
  };
113
113
  const isConfigMode = !!this.context.flowSettingsEnabled;
114
+ const fieldPathArray = this.context.fieldPathArray ?? this.parent?.context?.fieldPathArray;
114
115
  return (
115
116
  <SubTableField
116
117
  {...this.props}
@@ -121,8 +122,8 @@ export class SubTableFieldModel extends AssociationFieldModel {
121
122
  parentItem={this.context.item}
122
123
  filterTargetKey={this.collection.filterTargetKey}
123
124
  formValuesChangeEmitter={this.context.blockModel?.emitter}
124
- fieldPathArray={this.parent?.context?.fieldPathArray}
125
- getCurrentValue={this.getCurrentValue}
125
+ fieldPathArray={fieldPathArray}
126
+ getCurrentValue={() => this.getCurrentValue()}
126
127
  />
127
128
  );
128
129
  }
@@ -191,7 +191,15 @@ export class ClickableFieldModel extends FieldModel {
191
191
  }
192
192
 
193
193
  renderInDisplayStyle(value, record?, isToMany?, wrap?) {
194
- const { clickToOpen = false, displayStyle, titleField, overflowMode, disabled, ...restProps } = this.props;
194
+ const {
195
+ clickToOpen = false,
196
+ displayStyle,
197
+ titleField,
198
+ overflowMode,
199
+ disabled,
200
+ timeFormat,
201
+ ...restProps
202
+ } = this.props;
195
203
  const titleCollectionField = titleField
196
204
  ? this.context.collectionField?.targetCollection?.getField?.(titleField) || this.context.collectionField
197
205
  : this.context.collectionField;
@@ -15,7 +15,7 @@ import { ClickableFieldModel } from './ClickableFieldModel';
15
15
  export class DisplayTimeFieldModel extends ClickableFieldModel {
16
16
  public renderComponent(value) {
17
17
  const { prefix, suffix, style, className } = this.props;
18
- const format = this.props['format'] || 'HH:mm:ss';
18
+ const format = this.props['timeFormat'] || this.props['format'] || 'HH:mm:ss';
19
19
  const result = value && dayjs(value, 'HH:mm:ss').format(format);
20
20
  return (
21
21
  <span className={className} style={style}>
@@ -15,7 +15,7 @@ import { FieldModel } from '../base/FieldModel';
15
15
  import { MobileTimePicker } from './mobile-components/MobileTimePicker';
16
16
 
17
17
  const TimePickerCom = (props) => {
18
- const format = props['format'] || 'HH:mm:ss';
18
+ const format = props['timeFormat'] || props['format'] || 'HH:mm:ss';
19
19
  const onChange = props.onChange;
20
20
  const ctx = useFlowModelContext();
21
21
  const componentProps = {
@@ -0,0 +1,61 @@
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 { FlowEngine, FlowModelProvider } from '@nocobase/flow-engine';
11
+ import { render, screen } from '@testing-library/react';
12
+ import { describe, expect, it } from 'vitest';
13
+ import React from 'react';
14
+ import { DisplayTimeFieldModel } from '../DisplayTimeFieldModel';
15
+ import { TimeFieldModel } from '../TimeFieldModel';
16
+
17
+ describe('TimeFieldModel', () => {
18
+ it('uses timeFormat before format when rendering the editable time picker', () => {
19
+ const engine = new FlowEngine();
20
+ engine.registerModels({ TimeFieldModel });
21
+
22
+ const model = engine.createModel<TimeFieldModel>({
23
+ use: TimeFieldModel,
24
+ uid: 'time-field-format',
25
+ props: {
26
+ value: '13:05:06',
27
+ format: 'HH:mm:ss',
28
+ timeFormat: 'hh:mm:ss a',
29
+ },
30
+ });
31
+
32
+ render(
33
+ <FlowModelProvider model={model}>
34
+ <>{model.render()}</>
35
+ </FlowModelProvider>,
36
+ );
37
+
38
+ expect(screen.getByDisplayValue('01:05:06 pm')).toBeInTheDocument();
39
+ });
40
+ });
41
+
42
+ describe('DisplayTimeFieldModel', () => {
43
+ it('uses timeFormat before format when rendering read pretty time text', () => {
44
+ const engine = new FlowEngine();
45
+ engine.registerModels({ DisplayTimeFieldModel });
46
+
47
+ const model = engine.createModel<DisplayTimeFieldModel>({
48
+ use: DisplayTimeFieldModel,
49
+ uid: 'display-time-field-format',
50
+ props: {
51
+ value: '13:05:06',
52
+ format: 'HH:mm:ss',
53
+ timeFormat: 'hh:mm:ss a',
54
+ },
55
+ });
56
+
57
+ render(model.render());
58
+
59
+ expect(screen.getByText('01:05:06 pm')).toBeInTheDocument();
60
+ });
61
+ });
@@ -9,11 +9,26 @@
9
9
 
10
10
  import { useFlowModelContext } from '@nocobase/flow-engine';
11
11
  import { dayjs } from '@nocobase/utils/client';
12
+ import type { Dayjs } from 'dayjs';
12
13
  import { DatePicker } from 'antd';
13
14
  import { DatePicker as AntdMobileDatePicker } from 'antd-mobile';
14
15
  import React from 'react';
15
16
  import { useCallback, useState } from 'react';
16
17
 
18
+ type DateValue = string | number | Date | Dayjs | null | undefined;
19
+
20
+ function toNativeDate(value: DateValue): Date | null {
21
+ if (value === null || value === undefined || value === '') {
22
+ return null;
23
+ }
24
+ if (value instanceof Date) {
25
+ return Number.isNaN(value.getTime()) ? null : value;
26
+ }
27
+
28
+ const parsed = dayjs.isDayjs(value) ? value : dayjs(value);
29
+ return parsed.isValid() ? parsed.toDate() : null;
30
+ }
31
+
17
32
  export const MobileDatePicker = (props) => {
18
33
  const {
19
34
  value,
@@ -56,9 +71,9 @@ export const MobileDatePicker = (props) => {
56
71
  return data;
57
72
  }
58
73
  }, []);
59
- // Convert dayjs min/max to native Date for Antd Mobile Picker
60
- const minDate = min ? (min as any).toDate() : new Date(1950, 0, 1);
61
- const maxDate = max ? (max as any).toDate() : new Date(2050, 11, 31);
74
+ const mobileValue = toNativeDate(value);
75
+ const minDate = toNativeDate(min) ?? new Date(1950, 0, 1);
76
+ const maxDate = toNativeDate(max) ?? new Date(2050, 11, 31);
62
77
 
63
78
  return (
64
79
  <>
@@ -78,6 +93,7 @@ export const MobileDatePicker = (props) => {
78
93
  cancelText={t('Cancel')}
79
94
  confirmText={t('Confirm')}
80
95
  visible={visible}
96
+ value={mobileValue}
81
97
  title={<a onClick={handleClear}>{t('Clear')}</a>}
82
98
  onClose={() => {
83
99
  setVisible(false);