@nocobase/client-v2 2.1.0-beta.37 → 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.
- package/es/Application.d.ts +1 -0
- package/es/BaseApplication.d.ts +3 -0
- package/es/RouterManager.d.ts +1 -0
- package/es/components/KeepAlive.d.ts +22 -0
- package/es/components/RouterBridge.d.ts +9 -0
- package/es/data-source/ExtendCollectionsProvider.d.ts +28 -2
- package/es/flow/FlowPage.d.ts +2 -1
- package/es/flow/admin-shell/AdminLayoutRouteCoordinator.d.ts +8 -40
- package/es/flow/admin-shell/BaseLayoutModel.d.ts +89 -0
- package/es/flow/admin-shell/BaseLayoutRouteCoordinator.d.ts +74 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.d.ts +12 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutModel.d.ts +7 -92
- package/es/flow/admin-shell/admin-layout/index.d.ts +2 -0
- package/es/flow/admin-shell/useAdminLayoutRoutePage.d.ts +2 -2
- package/es/flow/admin-shell/useLayoutRoutePage.d.ts +23 -0
- package/es/flow/components/FlowRoute.d.ts +10 -1
- package/es/flow/index.d.ts +4 -0
- package/es/flow/models/base/PageModel/PageModel.d.ts +3 -1
- package/es/flow/models/blocks/form/FormActionGroupModel.d.ts +1 -0
- package/es/flow/models/blocks/table/TableBlockModel.d.ts +10 -0
- package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.d.ts +1 -1
- package/es/index.d.ts +1 -0
- package/es/index.mjs +484 -437
- package/es/layout-manager/LayoutContentRoute.d.ts +14 -0
- package/es/layout-manager/LayoutManager.d.ts +22 -0
- package/es/layout-manager/LayoutRoute.d.ts +14 -0
- package/es/layout-manager/index.d.ts +13 -0
- package/es/layout-manager/types.d.ts +20 -0
- package/es/layout-manager/utils.d.ts +14 -0
- package/es/settings-center/index.d.ts +1 -1
- package/es/settings-center/plugin-manager/BulkEnableButton.d.ts +15 -0
- package/es/settings-center/plugin-manager/PluginCard.d.ts +15 -0
- package/es/settings-center/plugin-manager/PluginDetail.d.ts +16 -0
- package/es/settings-center/{PluginManagerPage.d.ts → plugin-manager/index.d.ts} +1 -7
- package/es/settings-center/plugin-manager/types.d.ts +34 -0
- package/lib/index.js +484 -437
- package/package.json +8 -7
- package/src/Application.tsx +27 -12
- package/src/BaseApplication.tsx +6 -0
- package/src/PluginSettingsManager.ts +1 -1
- package/src/RouterManager.tsx +17 -1
- package/src/__tests__/PluginSettingsManager.test.ts +41 -2
- package/src/__tests__/app.test.tsx +8 -1
- package/src/__tests__/globalDeps.test.ts +1 -0
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +45 -2
- package/src/__tests__/plugin-manager.test.tsx +177 -0
- package/src/__tests__/settings-center.test.tsx +24 -2
- package/src/components/KeepAlive.tsx +131 -0
- package/src/components/RouterBridge.tsx +28 -4
- package/src/components/__tests__/KeepAlive.test.tsx +63 -0
- package/src/components/__tests__/RouterBridge.test.tsx +27 -0
- package/src/data-source/ExtendCollectionsProvider.tsx +94 -20
- package/src/data-source/__tests__/ExtendCollectionsProvider.test.tsx +264 -0
- package/src/flow/FlowPage.tsx +35 -7
- package/src/flow/__tests__/FlowPage.test.tsx +79 -0
- package/src/flow/__tests__/FlowRoute.test.tsx +529 -2
- package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +191 -0
- package/src/flow/actions/__tests__/openView.subModelKey.test.tsx +33 -0
- package/src/flow/actions/aclCheck.tsx +4 -0
- package/src/flow/actions/aclCheckRefresh.tsx +4 -0
- package/src/flow/actions/dateTimeFormat.tsx +12 -8
- package/src/flow/actions/linkageRules.tsx +122 -0
- package/src/flow/actions/openView.tsx +28 -4
- package/src/flow/admin-shell/AdminLayoutRouteCoordinator.ts +11 -329
- package/src/flow/admin-shell/BaseLayoutModel.tsx +455 -0
- package/src/flow/admin-shell/BaseLayoutRouteCoordinator.ts +502 -0
- package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +547 -3
- package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +4 -4
- package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +160 -0
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +0 -12
- package/src/flow/admin-shell/admin-layout/AdminLayoutModel.tsx +28 -201
- package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +11 -2
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +1 -26
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +149 -27
- package/src/flow/admin-shell/admin-layout/index.ts +2 -0
- package/src/flow/admin-shell/useAdminLayoutRoutePage.ts +10 -26
- package/src/flow/admin-shell/useLayoutRoutePage.ts +61 -0
- package/src/flow/components/AdminLayout.tsx +4 -154
- package/src/flow/components/FlowRoute.tsx +105 -15
- package/src/flow/index.ts +4 -0
- package/src/flow/models/base/ActionModel.tsx +8 -1
- package/src/flow/models/base/PageModel/PageModel.tsx +51 -18
- package/src/flow/models/base/PageModel/RootPageModel.tsx +6 -13
- package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +102 -1
- package/src/flow/models/base/RouteModel.tsx +1 -1
- package/src/flow/models/blocks/form/FormActionGroupModel.tsx +14 -0
- package/src/flow/models/blocks/form/FormItemModel.tsx +8 -1
- package/src/flow/models/blocks/form/__tests__/FormActionGroupModel.test.ts +46 -0
- package/src/flow/models/blocks/form/submitValues.ts +4 -1
- package/src/flow/models/blocks/table/TableBlockModel.tsx +118 -16
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.rowSelection.test.tsx +114 -0
- package/src/flow/models/fields/AssociationFieldModel/SubFormFieldModel.tsx +7 -1
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +1 -1
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +6 -5
- package/src/flow/models/fields/ClickableFieldModel.tsx +9 -1
- package/src/flow/models/fields/DisplayTimeFieldModel.tsx +1 -1
- package/src/flow/models/fields/TimeFieldModel.tsx +1 -1
- package/src/flow/models/fields/__tests__/TimeFieldModel.test.tsx +61 -0
- package/src/flow/models/fields/mobile-components/MobileDatePicker.tsx +19 -3
- package/src/flow/models/fields/mobile-components/__tests__/MobileDatePicker.test.tsx +94 -0
- package/src/flow/models/topbar/TopbarActionModel.tsx +1 -1
- package/src/flow/utils/__tests__/dateTimeFormat.test.ts +91 -0
- package/src/index.ts +1 -0
- package/src/layout-manager/LayoutContentRoute.tsx +90 -0
- package/src/layout-manager/LayoutManager.tsx +185 -0
- package/src/layout-manager/LayoutRoute.tsx +138 -0
- package/src/layout-manager/__tests__/LayoutManager.test.tsx +335 -0
- package/src/layout-manager/__tests__/LayoutRoute.test.tsx +473 -0
- package/src/layout-manager/index.ts +14 -0
- package/src/layout-manager/types.ts +22 -0
- package/src/layout-manager/utils.ts +37 -0
- package/src/nocobase-buildin-plugin/index.tsx +56 -48
- package/src/settings-center/index.ts +1 -1
- package/src/settings-center/plugin-manager/BulkEnableButton.tsx +111 -0
- package/src/settings-center/plugin-manager/PluginCard.tsx +270 -0
- package/src/settings-center/plugin-manager/PluginDetail.tsx +195 -0
- package/src/settings-center/plugin-manager/index.tsx +254 -0
- package/src/settings-center/plugin-manager/types.ts +35 -0
- package/src/settings-center/utils.tsx +8 -1
- package/src/theme/__tests__/globalStyles.test.ts +24 -0
- package/src/theme/globalStyles.ts +10 -0
- package/src/utils/globalDeps.ts +2 -0
- package/src/settings-center/PluginManagerPage.tsx +0 -162
|
@@ -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
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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
|
-
|
|
411
|
+
nextIndex = nextIndex + (current - 1) * pageSize + 1;
|
|
394
412
|
} else {
|
|
395
|
-
|
|
413
|
+
nextIndex = nextIndex + 1;
|
|
396
414
|
}
|
|
397
|
-
|
|
398
|
-
|
|
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]: !
|
|
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
|
-
{
|
|
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
|
|
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 =
|
|
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={
|
|
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]),
|
|
@@ -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={
|
|
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 {
|
|
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
|
-
|
|
60
|
-
const minDate =
|
|
61
|
-
const maxDate =
|
|
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);
|
|
@@ -0,0 +1,94 @@
|
|
|
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 React from 'react';
|
|
11
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
12
|
+
import { act, fireEvent, render, screen } from '@nocobase/test/client';
|
|
13
|
+
import { dayjs } from '@nocobase/utils/client';
|
|
14
|
+
import { MobileDatePicker } from '../MobileDatePicker';
|
|
15
|
+
|
|
16
|
+
type MockDatePickerProps = Record<string, unknown> & {
|
|
17
|
+
value?: unknown;
|
|
18
|
+
visible?: boolean;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const mockState = vi.hoisted(
|
|
22
|
+
(): {
|
|
23
|
+
antdDatePickerProps?: MockDatePickerProps;
|
|
24
|
+
mobileDatePickerProps?: MockDatePickerProps;
|
|
25
|
+
} => ({
|
|
26
|
+
antdDatePickerProps: undefined,
|
|
27
|
+
mobileDatePickerProps: undefined,
|
|
28
|
+
}),
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
vi.mock('@nocobase/flow-engine', async () => {
|
|
32
|
+
const actual = await vi.importActual<typeof import('@nocobase/flow-engine')>('@nocobase/flow-engine');
|
|
33
|
+
return {
|
|
34
|
+
...actual,
|
|
35
|
+
useFlowModelContext: () => ({
|
|
36
|
+
t: (value: string) => value,
|
|
37
|
+
}),
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
vi.mock('antd', async () => {
|
|
42
|
+
const actual = await vi.importActual<typeof import('antd')>('antd');
|
|
43
|
+
return {
|
|
44
|
+
...actual,
|
|
45
|
+
DatePicker: (props: MockDatePickerProps) => {
|
|
46
|
+
mockState.antdDatePickerProps = props;
|
|
47
|
+
return <div data-testid="antd-date-picker" />;
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
vi.mock('antd-mobile', () => ({
|
|
53
|
+
DatePicker: (props: MockDatePickerProps) => {
|
|
54
|
+
mockState.mobileDatePickerProps = props;
|
|
55
|
+
return props.visible ? <div data-testid="mobile-date-picker" /> : null;
|
|
56
|
+
},
|
|
57
|
+
}));
|
|
58
|
+
|
|
59
|
+
describe('MobileDatePicker', () => {
|
|
60
|
+
beforeEach(() => {
|
|
61
|
+
mockState.antdDatePickerProps = undefined;
|
|
62
|
+
mockState.mobileDatePickerProps = undefined;
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('uses the form value as the mobile popup value', () => {
|
|
66
|
+
render(
|
|
67
|
+
<MobileDatePicker
|
|
68
|
+
value={dayjs('2037-03-06 12:34:56')}
|
|
69
|
+
showTime
|
|
70
|
+
picker="date"
|
|
71
|
+
timeFormat="HH:mm:ss"
|
|
72
|
+
onChange={vi.fn()}
|
|
73
|
+
/>,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const trigger = screen.getByTestId('antd-date-picker').parentElement as HTMLElement;
|
|
77
|
+
act(() => {
|
|
78
|
+
fireEvent.click(trigger);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const popupValue = mockState.mobileDatePickerProps?.value;
|
|
82
|
+
expect(screen.getByTestId('mobile-date-picker')).toBeInTheDocument();
|
|
83
|
+
expect(popupValue).toBeInstanceOf(Date);
|
|
84
|
+
if (!(popupValue instanceof Date)) {
|
|
85
|
+
throw new Error('Expected popup value to be a Date');
|
|
86
|
+
}
|
|
87
|
+
expect(popupValue.getFullYear()).toBe(2037);
|
|
88
|
+
expect(popupValue.getMonth()).toBe(2);
|
|
89
|
+
expect(popupValue.getDate()).toBe(6);
|
|
90
|
+
expect(popupValue.getHours()).toBe(12);
|
|
91
|
+
expect(popupValue.getMinutes()).toBe(34);
|
|
92
|
+
expect(popupValue.getSeconds()).toBe(56);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -282,7 +282,7 @@ const UIEditorTopbarAction = observer(
|
|
|
282
282
|
const { token } = theme.useToken();
|
|
283
283
|
const customToken = token as CustomToken;
|
|
284
284
|
const designable = !!flowEngine.context.flowSettingsEnabled;
|
|
285
|
-
const isMobileLayout = !!
|
|
285
|
+
const isMobileLayout = !!model.context.isMobileLayout;
|
|
286
286
|
|
|
287
287
|
useHotkeys(
|
|
288
288
|
'ctrl+shift+u',
|