@nocobase/client-v2 2.1.0-alpha.24 → 2.1.0-alpha.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/es/flow/models/base/BlockGridModel.d.ts +2 -1
- package/es/flow/models/blocks/form/QuickEditFormModel.d.ts +7 -1
- package/es/flow/models/blocks/form/value-runtime/rules.d.ts +5 -0
- package/es/flow/models/blocks/form/value-runtime/runtime.d.ts +12 -0
- package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/rowIdentity.d.ts +18 -0
- package/es/index.mjs +84 -84
- package/lib/index.js +84 -84
- package/package.json +5 -5
- package/src/flow/models/base/BlockGridModel.tsx +48 -2
- package/src/flow/models/base/__tests__/BlockGridModel.selectSceneAddBlock.test.ts +83 -0
- package/src/flow/models/blocks/form/QuickEditFormModel.tsx +39 -16
- package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +293 -0
- package/src/flow/models/blocks/form/value-runtime/__tests__/subtable-nested.test.ts +3 -2
- package/src/flow/models/blocks/form/value-runtime/rules.ts +66 -14
- package/src/flow/models/blocks/form/value-runtime/runtime.ts +285 -12
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +10 -2
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +46 -22
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/rowIdentity.ts +70 -0
- package/src/flow/models/fields/AssociationFieldModel/__tests__/SubTableRowIdentity.test.ts +45 -0
|
@@ -14,12 +14,11 @@ import { useTranslation } from 'react-i18next';
|
|
|
14
14
|
import { PlusOutlined } from '@ant-design/icons';
|
|
15
15
|
import React, { useEffect, useMemo, useState } from 'react';
|
|
16
16
|
import { ActionWithoutPermission } from '../../../base/ActionModel';
|
|
17
|
-
import {
|
|
17
|
+
import { getSubTableRowIdentity, normalizeSubTableRows } from './rowIdentity';
|
|
18
18
|
|
|
19
19
|
export function SubTableField(props) {
|
|
20
20
|
const { t } = useTranslation();
|
|
21
21
|
const {
|
|
22
|
-
value = [],
|
|
23
22
|
onChange,
|
|
24
23
|
columns,
|
|
25
24
|
disabled,
|
|
@@ -39,12 +38,25 @@ export function SubTableField(props) {
|
|
|
39
38
|
} = props;
|
|
40
39
|
const [currentPage, setCurrentPage] = useState(1);
|
|
41
40
|
const [currentPageSize, setCurrentPageSize] = useState(pageSize);
|
|
41
|
+
const rawCurrentValue = getCurrentValue();
|
|
42
|
+
const currentValue = useMemo(() => normalizeSubTableRows(rawCurrentValue), [rawCurrentValue]);
|
|
43
|
+
const getRecordIdentity = React.useCallback(
|
|
44
|
+
(record: any) => getSubTableRowIdentity(record, filterTargetKey),
|
|
45
|
+
[filterTargetKey],
|
|
46
|
+
);
|
|
42
47
|
useEffect(() => {
|
|
43
48
|
setCurrentPageSize(pageSize);
|
|
44
49
|
}, [pageSize]);
|
|
45
50
|
useEffect(() => {
|
|
46
51
|
resetPage && setCurrentPage(1);
|
|
47
52
|
}, [resetPage]);
|
|
53
|
+
const applyValue = React.useCallback((nextValue: any) => onChange?.(normalizeSubTableRows(nextValue)), [onChange]);
|
|
54
|
+
const getLatestValue = React.useCallback(() => normalizeSubTableRows(getCurrentValue()), [getCurrentValue]);
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (currentValue !== rawCurrentValue) {
|
|
57
|
+
applyValue(currentValue);
|
|
58
|
+
}
|
|
59
|
+
}, [applyValue, currentValue, rawCurrentValue]);
|
|
48
60
|
|
|
49
61
|
// 前端分页
|
|
50
62
|
const pagination = useMemo(() => {
|
|
@@ -56,7 +68,7 @@ export function SubTableField(props) {
|
|
|
56
68
|
},
|
|
57
69
|
current: currentPage,
|
|
58
70
|
pageSize: currentPageSize,
|
|
59
|
-
total:
|
|
71
|
+
total: currentValue.length,
|
|
60
72
|
onChange: (page, size) => {
|
|
61
73
|
setCurrentPage(page);
|
|
62
74
|
setCurrentPageSize(size);
|
|
@@ -66,38 +78,36 @@ export function SubTableField(props) {
|
|
|
66
78
|
return t('Total {{count}} items', { count: total });
|
|
67
79
|
},
|
|
68
80
|
} as any;
|
|
69
|
-
}, [currentPage, currentPageSize,
|
|
81
|
+
}, [currentPage, currentPageSize, currentValue.length]);
|
|
70
82
|
|
|
71
83
|
// 新增一行
|
|
72
84
|
const handleAdd = () => {
|
|
73
85
|
if (allowCreate === false) return;
|
|
74
86
|
|
|
75
|
-
const currentValue = getCurrentValue();
|
|
76
87
|
const newRow = {
|
|
77
88
|
__is_new__: true,
|
|
78
89
|
};
|
|
79
90
|
columns.forEach((col) => {
|
|
80
91
|
newRow[col.dataIndex] = undefined;
|
|
81
92
|
});
|
|
82
|
-
const newValue = [...
|
|
93
|
+
const newValue = [...getLatestValue(), newRow];
|
|
83
94
|
setCurrentPage(Math.ceil(newValue.length / currentPageSize));
|
|
84
|
-
|
|
95
|
+
applyValue(newValue);
|
|
85
96
|
};
|
|
86
97
|
|
|
87
98
|
// 删除行
|
|
88
99
|
const handleDelete = (index: number) => {
|
|
89
|
-
const newValue = [...
|
|
100
|
+
const newValue = [...getLatestValue()];
|
|
90
101
|
newValue.splice(index, 1);
|
|
91
102
|
const lastPage = Math.ceil(newValue.length / currentPageSize);
|
|
92
103
|
setCurrentPage(currentPage > lastPage ? lastPage : currentPage);
|
|
93
|
-
|
|
104
|
+
applyValue(newValue);
|
|
94
105
|
};
|
|
95
106
|
|
|
96
107
|
// 编辑单元格
|
|
97
108
|
const handleCellChange = (rowIdx, dataIndex, cellValue) => {
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
onChange?.(newData);
|
|
109
|
+
const newData = getLatestValue().map((row, idx) => (idx === rowIdx ? { ...row, [dataIndex]: cellValue } : row));
|
|
110
|
+
applyValue(newData);
|
|
101
111
|
};
|
|
102
112
|
|
|
103
113
|
// 渲染可编辑单元格
|
|
@@ -106,20 +116,25 @@ export function SubTableField(props) {
|
|
|
106
116
|
...col,
|
|
107
117
|
render: (text, record, rowIdx) => {
|
|
108
118
|
const pageRowIdx = (currentPage - 1) * currentPageSize + rowIdx;
|
|
119
|
+
const rowIdentity = getRecordIdentity(record) ?? `row:${pageRowIdx}`;
|
|
120
|
+
// row identity keeps logical rows stable, while binding key still follows
|
|
121
|
+
// the current array index so Form.Item can rebind after reordering/removal.
|
|
122
|
+
const rowBindingKey = `${rowIdentity}:${pageRowIdx}`;
|
|
123
|
+
const columnKey = col.dataIndex ?? col.key ?? 'cell';
|
|
109
124
|
if (!col.render) {
|
|
110
125
|
return;
|
|
111
126
|
}
|
|
112
|
-
return col
|
|
127
|
+
return col.render({
|
|
113
128
|
record,
|
|
114
129
|
rowIdx: pageRowIdx,
|
|
115
|
-
id: `field-${
|
|
130
|
+
id: `field-${String(columnKey)}-${rowBindingKey}`,
|
|
116
131
|
value: text,
|
|
117
132
|
parentFieldIndex,
|
|
118
133
|
parentItem,
|
|
119
134
|
onChange: (value) => {
|
|
120
|
-
handleCellChange(pageRowIdx, col.dataIndex, value?.target?.value
|
|
135
|
+
handleCellChange(pageRowIdx, col.dataIndex, value?.target?.value ?? value);
|
|
121
136
|
},
|
|
122
|
-
['aria-describedby']: `field-${
|
|
137
|
+
['aria-describedby']: `field-${String(columnKey)}-${rowBindingKey}`,
|
|
123
138
|
});
|
|
124
139
|
},
|
|
125
140
|
}))
|
|
@@ -137,8 +152,17 @@ export function SubTableField(props) {
|
|
|
137
152
|
}
|
|
138
153
|
return (
|
|
139
154
|
<div
|
|
155
|
+
onMouseDown={(event) => {
|
|
156
|
+
const activeElement = document.activeElement as HTMLElement | null;
|
|
157
|
+
if (!activeElement || event.currentTarget.contains(activeElement)) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
activeElement.blur?.();
|
|
161
|
+
}}
|
|
140
162
|
onClick={() => {
|
|
141
|
-
|
|
163
|
+
setTimeout(() => {
|
|
164
|
+
handleDelete(pageRowIdx);
|
|
165
|
+
});
|
|
142
166
|
}}
|
|
143
167
|
>
|
|
144
168
|
<CloseOutlined style={{ cursor: 'pointer', color: 'gray' }} />
|
|
@@ -150,17 +174,17 @@ export function SubTableField(props) {
|
|
|
150
174
|
.filter(Boolean);
|
|
151
175
|
|
|
152
176
|
const pagedDataSource = useMemo(() => {
|
|
153
|
-
if (!
|
|
177
|
+
if (!currentValue.length) return [];
|
|
154
178
|
|
|
155
179
|
const start = (currentPage - 1) * currentPageSize;
|
|
156
|
-
return
|
|
157
|
-
}, [
|
|
180
|
+
return currentValue.slice(start, start + currentPageSize);
|
|
181
|
+
}, [currentValue, currentPage, currentPageSize]);
|
|
158
182
|
return (
|
|
159
183
|
<Form.Item>
|
|
160
184
|
<Table
|
|
161
185
|
dataSource={pagedDataSource}
|
|
162
186
|
columns={editableColumns}
|
|
163
|
-
rowKey={(record) =>
|
|
187
|
+
rowKey={(record) => getRecordIdentity(record) ?? ''}
|
|
164
188
|
tableLayout="fixed"
|
|
165
189
|
scroll={{ x: 'max-content' }}
|
|
166
190
|
pagination={pagination}
|
|
@@ -179,7 +203,7 @@ export function SubTableField(props) {
|
|
|
179
203
|
</span>
|
|
180
204
|
),
|
|
181
205
|
}}
|
|
182
|
-
components={components
|
|
206
|
+
components={components ?? {}}
|
|
183
207
|
className={css`
|
|
184
208
|
.ant-table-cell-ellipsis.ant-table-cell-fix-right-first .ant-table-cell-content {
|
|
185
209
|
display: inline;
|
|
@@ -0,0 +1,70 @@
|
|
|
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 { uid } from '@formily/shared';
|
|
11
|
+
|
|
12
|
+
type FilterTargetKey = string | string[] | null | undefined;
|
|
13
|
+
type SubTableRow = {
|
|
14
|
+
__is_new__?: boolean;
|
|
15
|
+
id?: any;
|
|
16
|
+
[key: string]: any;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const SUB_TABLE_TEMP_ROW_KEY = '__index__';
|
|
20
|
+
|
|
21
|
+
function getPersistedRowKey(record: SubTableRow, filterTargetKey: FilterTargetKey) {
|
|
22
|
+
if (!filterTargetKey) return null;
|
|
23
|
+
|
|
24
|
+
if (Array.isArray(filterTargetKey)) {
|
|
25
|
+
const values = filterTargetKey.map((k) => record?.[k]);
|
|
26
|
+
if (values.some((v) => v == null)) return null;
|
|
27
|
+
return values.map((v) => String(v)).join('__');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const value = record?.[filterTargetKey];
|
|
31
|
+
return value == null ? null : String(value);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function getSubTableRowIdentity(record: SubTableRow, filterTargetKey: FilterTargetKey) {
|
|
35
|
+
const tempKey = record?.[SUB_TABLE_TEMP_ROW_KEY];
|
|
36
|
+
if (record?.__is_new__ && tempKey != null && tempKey !== '') {
|
|
37
|
+
return `tmp:${String(tempKey)}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const persistedKey = getPersistedRowKey(record, filterTargetKey);
|
|
41
|
+
if (persistedKey != null) {
|
|
42
|
+
return `pk:${persistedKey}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (tempKey != null && tempKey !== '') {
|
|
46
|
+
return `tmp:${String(tempKey)}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function normalizeSubTableRows(rows: SubTableRow[]) {
|
|
53
|
+
if (!rows.length) return rows;
|
|
54
|
+
|
|
55
|
+
let changed = false;
|
|
56
|
+
const normalized = rows.map((row) => {
|
|
57
|
+
const tempKey = row?.[SUB_TABLE_TEMP_ROW_KEY];
|
|
58
|
+
if (!row.__is_new__ || (tempKey != null && tempKey !== '')) {
|
|
59
|
+
return row;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
changed = true;
|
|
63
|
+
return {
|
|
64
|
+
...row,
|
|
65
|
+
[SUB_TABLE_TEMP_ROW_KEY]: uid(),
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
return changed ? normalized : rows;
|
|
70
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, expect, it } from 'vitest';
|
|
11
|
+
import {
|
|
12
|
+
getSubTableRowIdentity,
|
|
13
|
+
normalizeSubTableRows,
|
|
14
|
+
SUB_TABLE_TEMP_ROW_KEY,
|
|
15
|
+
} from '../SubTableFieldModel/rowIdentity';
|
|
16
|
+
|
|
17
|
+
describe('SubTable row identity', () => {
|
|
18
|
+
it('keeps temp identity for unsaved rows and fills it when missing', () => {
|
|
19
|
+
const rows = [
|
|
20
|
+
{
|
|
21
|
+
__is_new__: true,
|
|
22
|
+
[SUB_TABLE_TEMP_ROW_KEY]: 'tmp-1',
|
|
23
|
+
name: 'admin',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
__is_new__: true,
|
|
27
|
+
[SUB_TABLE_TEMP_ROW_KEY]: 'tmp-existing',
|
|
28
|
+
title: 'kept',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
__is_new__: true,
|
|
32
|
+
title: 'needs-temp-key',
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
expect(getSubTableRowIdentity(rows[0], 'name')).toBe('tmp:tmp-1');
|
|
37
|
+
|
|
38
|
+
const normalized = normalizeSubTableRows(rows);
|
|
39
|
+
|
|
40
|
+
expect(normalized[1]).toBe(rows[1]);
|
|
41
|
+
expect(normalized[2]).not.toBe(rows[2]);
|
|
42
|
+
expect(normalized[2][SUB_TABLE_TEMP_ROW_KEY]).toBeTruthy();
|
|
43
|
+
expect(getSubTableRowIdentity(normalized[2], 'id')).toMatch(/^tmp:/);
|
|
44
|
+
});
|
|
45
|
+
});
|