@nocobase/client-v2 2.2.15 → 2.2.17
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/internal/utils/quickEditViewContainer.d.ts +27 -0
- package/es/index.mjs +62 -62
- package/lib/index.js +62 -62
- package/package.json +7 -7
- package/src/flow/internal/utils/__tests__/quickEditViewContainer.test.ts +52 -0
- package/src/flow/internal/utils/quickEditViewContainer.ts +46 -0
- package/src/flow/models/blocks/form/QuickEditFormModel.tsx +15 -0
- package/src/flow/models/blocks/form/__tests__/QuickEditFormModel.quickEdit.test.ts +44 -0
- package/src/flow/models/fields/AssociationFieldModel/CascadeSelectFieldModel.tsx +7 -1
- package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +2 -0
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +12 -10
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +1 -0
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableField.disabled.test.tsx +90 -0
- package/src/flow/models/fields/SelectFieldModel.tsx +5 -0
- package/src/flow/models/fields/__tests__/SelectFieldModel.test.tsx +29 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/client-v2",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.17",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"module": "es/index.mjs",
|
|
@@ -27,11 +27,11 @@
|
|
|
27
27
|
"@formily/antd-v5": "1.2.3",
|
|
28
28
|
"@formily/react": "^2.2.27",
|
|
29
29
|
"@formily/shared": "^2.2.27",
|
|
30
|
-
"@nocobase/evaluators": "2.2.
|
|
31
|
-
"@nocobase/flow-engine": "2.2.
|
|
32
|
-
"@nocobase/sdk": "2.2.
|
|
33
|
-
"@nocobase/shared": "2.2.
|
|
34
|
-
"@nocobase/utils": "2.2.
|
|
30
|
+
"@nocobase/evaluators": "2.2.17",
|
|
31
|
+
"@nocobase/flow-engine": "2.2.17",
|
|
32
|
+
"@nocobase/sdk": "2.2.17",
|
|
33
|
+
"@nocobase/shared": "2.2.17",
|
|
34
|
+
"@nocobase/utils": "2.2.17",
|
|
35
35
|
"ahooks": "^3.7.2",
|
|
36
36
|
"antd": "5.24.2",
|
|
37
37
|
"antd-style": "3.7.1",
|
|
@@ -49,5 +49,5 @@
|
|
|
49
49
|
"react-router-dom": "^6.30.1",
|
|
50
50
|
"zxing-wasm": "^3.1.3"
|
|
51
51
|
},
|
|
52
|
-
"gitHead": "
|
|
52
|
+
"gitHead": "08565a7d1c326c4c2d190150f303596dda9c6450"
|
|
53
53
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
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 { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
11
|
+
import { getQuickEditViewContainer, syncQuickEditPreventClose } from '../quickEditViewContainer';
|
|
12
|
+
|
|
13
|
+
describe('quickEditViewContainer', () => {
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
vi.useFakeTimers();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
vi.useRealTimers();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('only resolves the view container of a QuickEditFormModel parent', () => {
|
|
23
|
+
const viewContainer = { update: vi.fn() };
|
|
24
|
+
expect(getQuickEditViewContainer({ parent: { use: 'QuickEditFormModel', viewContainer } })).toBe(viewContainer);
|
|
25
|
+
expect(getQuickEditViewContainer({ parent: { use: 'EditFormModel', viewContainer } })).toBeUndefined();
|
|
26
|
+
expect(getQuickEditViewContainer({ parent: null })).toBeUndefined();
|
|
27
|
+
expect(getQuickEditViewContainer(undefined)).toBeUndefined();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('locks the popover while the dropdown is open and releases it after the closing click', () => {
|
|
31
|
+
const update = vi.fn();
|
|
32
|
+
const model = { parent: { use: 'QuickEditFormModel', viewContainer: { update } } };
|
|
33
|
+
|
|
34
|
+
syncQuickEditPreventClose(model, true);
|
|
35
|
+
expect(update).toHaveBeenCalledWith({ preventClose: true });
|
|
36
|
+
|
|
37
|
+
syncQuickEditPreventClose(model, false);
|
|
38
|
+
expect(update).toHaveBeenCalledTimes(1);
|
|
39
|
+
|
|
40
|
+
vi.runAllTimers();
|
|
41
|
+
expect(update).toHaveBeenCalledTimes(2);
|
|
42
|
+
expect(update).toHaveBeenLastCalledWith({ preventClose: false });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('is a no-op outside of quick edit', () => {
|
|
46
|
+
const update = vi.fn();
|
|
47
|
+
syncQuickEditPreventClose({ parent: { use: 'EditFormModel', viewContainer: { update } } }, true);
|
|
48
|
+
syncQuickEditPreventClose({ parent: { use: 'QuickEditFormModel' } }, true);
|
|
49
|
+
vi.runAllTimers();
|
|
50
|
+
expect(update).not.toHaveBeenCalled();
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -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
|
+
type QuickEditViewContainerLike = {
|
|
11
|
+
update?: (newConfig: { preventClose?: boolean }) => unknown;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type QuickEditFieldModelLike = {
|
|
15
|
+
parent?: {
|
|
16
|
+
use?: string;
|
|
17
|
+
viewContainer?: QuickEditViewContainerLike;
|
|
18
|
+
} | null;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function getQuickEditViewContainer(model: QuickEditFieldModelLike | undefined) {
|
|
22
|
+
const parent = model?.parent;
|
|
23
|
+
if (parent?.use !== 'QuickEditFormModel') {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
return parent.viewContainer;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Keeps the quick edit popover open while a nested dropdown (Select / Cascader) is open. The outside click that
|
|
31
|
+
* closes the dropdown would otherwise also close the popover and drop the unsaved edit. Releasing is deferred so the
|
|
32
|
+
* closing click itself never reaches the popover as a close request.
|
|
33
|
+
*/
|
|
34
|
+
export function syncQuickEditPreventClose(model: QuickEditFieldModelLike | undefined, dropdownOpen: boolean) {
|
|
35
|
+
const container = getQuickEditViewContainer(model);
|
|
36
|
+
if (!container?.update) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (dropdownOpen) {
|
|
40
|
+
container.update({ preventClose: true });
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
setTimeout(() => {
|
|
44
|
+
container.update?.({ preventClose: false });
|
|
45
|
+
}, 0);
|
|
46
|
+
}
|
|
@@ -36,6 +36,14 @@ const QUICK_EDIT_MOBILE_CONTENT_PADDING = '8px var(--nb-mobile-page-tabs-content
|
|
|
36
36
|
const QUICK_EDIT_MOBILE_ACTIONS_PADDING =
|
|
37
37
|
'8px var(--nb-mobile-page-tabs-content-padding, 12px) calc(80px + env(safe-area-inset-bottom, 0px))';
|
|
38
38
|
const QUICK_EDIT_MOBILE_MEDIA_QUERY = '(max-width: 768px)';
|
|
39
|
+
// Dropdown-style editors open upwards inside the desktop popover so the option list does not cover the Submit and
|
|
40
|
+
// Cancel buttons rendered right below the field. antd still flips the list downwards when there is no room above.
|
|
41
|
+
const QUICK_EDIT_DROPDOWN_UP_MODELS = [
|
|
42
|
+
'SelectFieldModel',
|
|
43
|
+
'RecordSelectFieldModel',
|
|
44
|
+
'CascadeSelectFieldModel',
|
|
45
|
+
'CascadeSelectListFieldModel',
|
|
46
|
+
];
|
|
39
47
|
|
|
40
48
|
type QuickEditViewBeforeClosePayload = {
|
|
41
49
|
result?: unknown;
|
|
@@ -435,6 +443,13 @@ QuickEditFormModel.registerFlow({
|
|
|
435
443
|
});
|
|
436
444
|
fieldModel.setProps(getQuickEditFieldProps(collectionField, ctx.model._fieldProps));
|
|
437
445
|
fieldModel.setProps({ sourceFieldModelUid: ctx.inputArgs.sourceFieldModelUid });
|
|
446
|
+
if (
|
|
447
|
+
!ctx.model.context.isMobileLayout &&
|
|
448
|
+
QUICK_EDIT_DROPDOWN_UP_MODELS.includes(use) &&
|
|
449
|
+
fieldModel.props.placement == null
|
|
450
|
+
) {
|
|
451
|
+
fieldModel.setProps({ placement: 'topLeft' });
|
|
452
|
+
}
|
|
438
453
|
ctx.model.context.defineProperty('collectionField', {
|
|
439
454
|
get: () => collectionField,
|
|
440
455
|
});
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
11
11
|
import { FlowEngine, FlowModel, SingleRecordResource } from '@nocobase/flow-engine';
|
|
12
12
|
import { QuickEditFormModel } from '../QuickEditFormModel';
|
|
13
|
+
import { SelectFieldModel } from '../../../fields/SelectFieldModel';
|
|
13
14
|
|
|
14
15
|
describe('QuickEditFormModel - quick edit save triggers API (regression)', () => {
|
|
15
16
|
let engine: FlowEngine;
|
|
@@ -414,4 +415,47 @@ describe('QuickEditFormModel - quick edit save triggers API (regression)', () =>
|
|
|
414
415
|
|
|
415
416
|
expect(api.request).toHaveBeenCalledTimes(1);
|
|
416
417
|
});
|
|
418
|
+
|
|
419
|
+
it('opens dropdown editors upwards in the desktop popover so they do not cover the actions', async () => {
|
|
420
|
+
engine.registerModels({ QuickEditFormModel, SelectFieldModel });
|
|
421
|
+
const ds = engine.context.dataSourceManager.getDataSource('main');
|
|
422
|
+
ds.addCollection({
|
|
423
|
+
name: 'users',
|
|
424
|
+
fields: [
|
|
425
|
+
{
|
|
426
|
+
name: 'tags',
|
|
427
|
+
type: 'array',
|
|
428
|
+
interface: 'multipleSelect',
|
|
429
|
+
uiSchema: { title: 'Tags', enum: [{ label: 'A', value: 'a' }] },
|
|
430
|
+
},
|
|
431
|
+
],
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
const desktopModel = engine.createModel<QuickEditFormModel>({
|
|
435
|
+
use: QuickEditFormModel,
|
|
436
|
+
uid: 'qe-desktop',
|
|
437
|
+
stepParams: {
|
|
438
|
+
quickEditFormSettings: {
|
|
439
|
+
init: { dataSourceKey: 'main', collectionName: 'users', fieldPath: 'tags' },
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
});
|
|
443
|
+
await desktopModel.applyFlow('quickEditFormSettings', { record: {} });
|
|
444
|
+
const desktopField = desktopModel.subModels.fields[0];
|
|
445
|
+
expect(desktopField.use).toBe('SelectFieldModel');
|
|
446
|
+
expect(desktopField.props.placement).toBe('topLeft');
|
|
447
|
+
|
|
448
|
+
const mobileModel = engine.createModel<QuickEditFormModel>({
|
|
449
|
+
use: QuickEditFormModel,
|
|
450
|
+
uid: 'qe-mobile',
|
|
451
|
+
stepParams: {
|
|
452
|
+
quickEditFormSettings: {
|
|
453
|
+
init: { dataSourceKey: 'main', collectionName: 'users', fieldPath: 'tags' },
|
|
454
|
+
},
|
|
455
|
+
},
|
|
456
|
+
});
|
|
457
|
+
mobileModel.context.defineProperty('isMobileLayout', { value: true });
|
|
458
|
+
await mobileModel.applyFlow('quickEditFormSettings', { record: {} });
|
|
459
|
+
expect(mobileModel.subModels.fields[0].props.placement).toBeUndefined();
|
|
460
|
+
});
|
|
417
461
|
});
|
|
@@ -25,6 +25,7 @@ import { arrayMove, SortableContext, verticalListSortingStrategy, useSortable }
|
|
|
25
25
|
import { CSS } from '@dnd-kit/utilities';
|
|
26
26
|
import { AssociationFieldModel } from './AssociationFieldModel';
|
|
27
27
|
import { transformNestedData } from '../ClickableFieldModel';
|
|
28
|
+
import { syncQuickEditPreventClose } from '../../../internal/utils/quickEditViewContainer';
|
|
28
29
|
|
|
29
30
|
type CascadeHydrateStatus = 'pending' | 'done';
|
|
30
31
|
|
|
@@ -373,7 +374,12 @@ export class CascadeSelectInnerFieldModel extends AssociationFieldModel {
|
|
|
373
374
|
this.setProps({ onPopupScroll: fn });
|
|
374
375
|
}
|
|
375
376
|
set onDropdownVisibleChange(fn) {
|
|
376
|
-
this.setProps({
|
|
377
|
+
this.setProps({
|
|
378
|
+
onDropdownVisibleChange: (visible: boolean) => {
|
|
379
|
+
syncQuickEditPreventClose(this, visible);
|
|
380
|
+
fn(visible);
|
|
381
|
+
},
|
|
382
|
+
});
|
|
377
383
|
}
|
|
378
384
|
set onSearch(fn) {
|
|
379
385
|
this.setProps({ onSearch: fn });
|
|
@@ -41,6 +41,7 @@ import { BlockSceneEnum } from '../../base/BlockModel';
|
|
|
41
41
|
import { ActionWithoutPermission } from '../../base/ActionModel';
|
|
42
42
|
import { EditFormModel } from '../../blocks';
|
|
43
43
|
import { hasAncestorModel } from './recordSelectSettingsUtils';
|
|
44
|
+
import { syncQuickEditPreventClose } from '../../../internal/utils/quickEditViewContainer';
|
|
44
45
|
|
|
45
46
|
function RemoteModelRenderer({ options }) {
|
|
46
47
|
const ctx = useFlowViewContext();
|
|
@@ -308,6 +309,7 @@ const LazySelect = (props: Readonly<LazySelectProps>) => {
|
|
|
308
309
|
}
|
|
309
310
|
setKeepOpenRuntime({ preventCloseOnSelect: false, open });
|
|
310
311
|
}
|
|
312
|
+
syncQuickEditPreventClose(model, open);
|
|
311
313
|
others.onDropdownVisibleChange?.(open);
|
|
312
314
|
}}
|
|
313
315
|
optionRender={({ data }) => {
|
package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx
CHANGED
|
@@ -209,7 +209,7 @@ function hasFormValueDrivenDataScopeField(fieldModel: any) {
|
|
|
209
209
|
}
|
|
210
210
|
|
|
211
211
|
const MemoFieldRenderer = React.memo(FieldModelRenderer, (prev, next) => {
|
|
212
|
-
return prev.value === next.value && prev.model === next.model;
|
|
212
|
+
return prev.value === next.value && prev.model === next.model && prev.disabled === next.disabled;
|
|
213
213
|
});
|
|
214
214
|
|
|
215
215
|
export function buildRowPathFromFieldIndex(fieldIndex: unknown): Array<string | number> | null {
|
|
@@ -345,11 +345,14 @@ interface CellProps {
|
|
|
345
345
|
memoKey?: string;
|
|
346
346
|
width?: number;
|
|
347
347
|
commitOnChange?: boolean;
|
|
348
|
+
disabled?: boolean;
|
|
348
349
|
}
|
|
349
350
|
|
|
350
351
|
const MemoCell: React.FC<CellProps> = React.memo(
|
|
351
|
-
({ value, record, rowIdx, id, parent, parentFieldIndex, rowFork, width, commitOnChange }) => {
|
|
352
|
+
({ value, record, rowIdx, id, parent, parentFieldIndex, rowFork, width, commitOnChange, disabled }) => {
|
|
352
353
|
const isNew = record?.__is_new__;
|
|
354
|
+
// 子表格整体被禁用时,列上的显示模式可能仍是“可编辑”,这里需要把父级的禁用状态一并合并进来,否则单元格仍可编辑。
|
|
355
|
+
const columnDisabled = disabled || parent.props.disabled;
|
|
353
356
|
return (
|
|
354
357
|
<div
|
|
355
358
|
style={{
|
|
@@ -442,9 +445,7 @@ const MemoCell: React.FC<CellProps> = React.memo(
|
|
|
442
445
|
style={{ marginBottom: 0 }}
|
|
443
446
|
showLabel={false}
|
|
444
447
|
disabled={
|
|
445
|
-
parent.props.
|
|
446
|
-
(!isNew && parent.props.aclDisabled) ||
|
|
447
|
-
(isNew && parent.props.aclCreateDisabled)
|
|
448
|
+
columnDisabled || (!isNew && parent.props.aclDisabled) || (isNew && parent.props.aclCreateDisabled)
|
|
448
449
|
}
|
|
449
450
|
>
|
|
450
451
|
{fork.constructor.isLargeField ? (
|
|
@@ -456,9 +457,7 @@ const MemoCell: React.FC<CellProps> = React.memo(
|
|
|
456
457
|
}}
|
|
457
458
|
defaultValue={value}
|
|
458
459
|
disabled={
|
|
459
|
-
parent.props.
|
|
460
|
-
(!isNew && parent.props.aclDisabled) ||
|
|
461
|
-
(isNew && parent.props.aclCreateDisabled)
|
|
460
|
+
columnDisabled || (!isNew && parent.props.aclDisabled) || (isNew && parent.props.aclCreateDisabled)
|
|
462
461
|
}
|
|
463
462
|
/>
|
|
464
463
|
) : (
|
|
@@ -481,7 +480,8 @@ const MemoCell: React.FC<CellProps> = React.memo(
|
|
|
481
480
|
prev.memoKey === next.memoKey &&
|
|
482
481
|
prev.width === next.width &&
|
|
483
482
|
prev.commitOnChange === next.commitOnChange &&
|
|
484
|
-
prev.rowIdx === next.rowIdx
|
|
483
|
+
prev.rowIdx === next.rowIdx &&
|
|
484
|
+
prev.disabled === next.disabled
|
|
485
485
|
);
|
|
486
486
|
},
|
|
487
487
|
);
|
|
@@ -740,7 +740,7 @@ export class SubTableColumnModel<
|
|
|
740
740
|
}
|
|
741
741
|
renderItem(): any {
|
|
742
742
|
return (props) => {
|
|
743
|
-
const { value, id, rowIdx, record, parentFieldIndex, parentItem } = props || {};
|
|
743
|
+
const { value, id, rowIdx, record, parentFieldIndex, parentItem, disabled: subTableDisabled } = props || {};
|
|
744
744
|
// 子表格列模型本身没有行级 fieldIndex,上下文中无法把 `roles.name` 解析成 `roles[0].name`,
|
|
745
745
|
// 导致“默认值/赋值规则”在对多关系字段下无法生效。
|
|
746
746
|
// 这里为每一行创建一个 column fork,并注入 fieldIndex,让规则引擎能够按行解析与写入。
|
|
@@ -754,6 +754,7 @@ export class SubTableColumnModel<
|
|
|
754
754
|
const fieldModel: any = this.subModels.field;
|
|
755
755
|
const cellModeKey = [
|
|
756
756
|
rowForkKey,
|
|
757
|
+
subTableDisabled ? 'disabled' : 'enabled',
|
|
757
758
|
this.props.pattern,
|
|
758
759
|
this.props.readPretty,
|
|
759
760
|
this.props.titleField,
|
|
@@ -828,6 +829,7 @@ export class SubTableColumnModel<
|
|
|
828
829
|
parentItem={parentItem}
|
|
829
830
|
rowFork={rowFork}
|
|
830
831
|
memoKey={cellModeKey}
|
|
832
|
+
disabled={subTableDisabled}
|
|
831
833
|
width={this.props.width}
|
|
832
834
|
commitOnChange={this.hasFormulaColumn || this.hasFormValueDrivenDataScopeColumn}
|
|
833
835
|
/>
|
|
@@ -0,0 +1,90 @@
|
|
|
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 { describe, expect, it, vi } from 'vitest';
|
|
12
|
+
import { render, screen } from '@nocobase/test/client';
|
|
13
|
+
import { SubTableField } from '../SubTableField';
|
|
14
|
+
|
|
15
|
+
vi.mock('react-i18next', async (importOriginal) => ({
|
|
16
|
+
...(await importOriginal<any>()),
|
|
17
|
+
useTranslation: () => ({
|
|
18
|
+
t: (value: string) => value,
|
|
19
|
+
}),
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
vi.mock('antd', async () => {
|
|
23
|
+
const actual = await vi.importActual<any>('antd');
|
|
24
|
+
return {
|
|
25
|
+
...actual,
|
|
26
|
+
Table: ({ dataSource = [], columns = [] }: any) => (
|
|
27
|
+
<div data-testid="subtable">
|
|
28
|
+
<div data-testid="column-keys">{columns.map((column: any) => String(column.key)).join(',')}</div>
|
|
29
|
+
{dataSource.map((record: any, rowIdx: number) => (
|
|
30
|
+
<div data-testid={`row-${rowIdx}`} key={record.__index__ || rowIdx}>
|
|
31
|
+
{columns.map((column: any) => (
|
|
32
|
+
<div data-testid={`cell-${rowIdx}-${String(column.dataIndex || column.key)}`} key={column.key}>
|
|
33
|
+
{column.render?.(record[column.dataIndex], record, rowIdx)}
|
|
34
|
+
</div>
|
|
35
|
+
))}
|
|
36
|
+
</div>
|
|
37
|
+
))}
|
|
38
|
+
</div>
|
|
39
|
+
),
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
function renderSubTableField(disabled?: boolean) {
|
|
44
|
+
const renderArgs: any[] = [];
|
|
45
|
+
const columns = [
|
|
46
|
+
{
|
|
47
|
+
key: 'name',
|
|
48
|
+
dataIndex: 'name',
|
|
49
|
+
render: (args: any) => {
|
|
50
|
+
renderArgs.push(args);
|
|
51
|
+
return <span>{args.value || 'empty'}</span>;
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
render(
|
|
57
|
+
<SubTableField
|
|
58
|
+
columns={columns}
|
|
59
|
+
disabled={disabled}
|
|
60
|
+
pageSize={10}
|
|
61
|
+
filterTargetKey="id"
|
|
62
|
+
fieldPathArray={['roles']}
|
|
63
|
+
getCurrentValue={() => [{ __index__: 'row-1', id: 1, name: 'admin' }]}
|
|
64
|
+
/>,
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
return renderArgs;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
describe('SubTableField disabled', () => {
|
|
71
|
+
it('passes the disabled state down to every column cell', () => {
|
|
72
|
+
const renderArgs = renderSubTableField(true);
|
|
73
|
+
|
|
74
|
+
expect(renderArgs).toHaveLength(1);
|
|
75
|
+
expect(renderArgs[0].disabled).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('keeps cells editable when the sub-table is not disabled', () => {
|
|
79
|
+
const renderArgs = renderSubTableField(false);
|
|
80
|
+
|
|
81
|
+
expect(renderArgs).toHaveLength(1);
|
|
82
|
+
expect(renderArgs[0].disabled).toBe(false);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('hides the remove column when the sub-table is disabled', () => {
|
|
86
|
+
renderSubTableField(true);
|
|
87
|
+
|
|
88
|
+
expect(screen.getByTestId('column-keys')).not.toHaveTextContent('delete');
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -13,6 +13,7 @@ import React from 'react';
|
|
|
13
13
|
import { FieldModel } from '../base/FieldModel';
|
|
14
14
|
import { MobileSelect } from './mobile-components/MobileSelect';
|
|
15
15
|
import { enumToOptions, getSelectedEnumLabels, translateOptionLabel } from '../../internal/utils/enumOptionsUtils';
|
|
16
|
+
import { syncQuickEditPreventClose } from '../../internal/utils/quickEditViewContainer';
|
|
16
17
|
|
|
17
18
|
const getOriginalEnumOptions = (model: SelectFieldModel) => {
|
|
18
19
|
const fromEnum = enumToOptions(model.context.collectionField?.uiSchema?.enum, model.translate) || [];
|
|
@@ -62,6 +63,10 @@ export class SelectFieldModel extends FieldModel {
|
|
|
62
63
|
this.props.onChange?.(nextValue?.value);
|
|
63
64
|
}}
|
|
64
65
|
options={options}
|
|
66
|
+
onDropdownVisibleChange={(open) => {
|
|
67
|
+
syncQuickEditPreventClose(this, open);
|
|
68
|
+
this.props.onDropdownVisibleChange?.(open);
|
|
69
|
+
}}
|
|
65
70
|
labelRender={(item) => item.label}
|
|
66
71
|
maxTagCount="responsive"
|
|
67
72
|
maxTagPlaceholder={(omittedValues) => (
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import React from 'react';
|
|
11
|
-
import { describe, expect, it } from 'vitest';
|
|
11
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
12
12
|
import { SelectFieldModel } from '../SelectFieldModel';
|
|
13
13
|
|
|
14
14
|
function mockT(text: string) {
|
|
@@ -40,4 +40,32 @@ describe('SelectFieldModel', () => {
|
|
|
40
40
|
|
|
41
41
|
expect(element.props.value).toEqual({ label: '是', value: true });
|
|
42
42
|
});
|
|
43
|
+
|
|
44
|
+
it('keeps the quick edit popover open while the dropdown is open', () => {
|
|
45
|
+
vi.useFakeTimers();
|
|
46
|
+
try {
|
|
47
|
+
const update = vi.fn();
|
|
48
|
+
const onDropdownVisibleChange = vi.fn();
|
|
49
|
+
const model = {
|
|
50
|
+
props: { value: undefined, onDropdownVisibleChange },
|
|
51
|
+
parent: { use: 'QuickEditFormModel', viewContainer: { update } },
|
|
52
|
+
context: { collectionField: { uiSchema: { enum: [] } } },
|
|
53
|
+
translate: mockT,
|
|
54
|
+
} as unknown as SelectFieldModel;
|
|
55
|
+
|
|
56
|
+
const element = SelectFieldModel.prototype.render.call(model) as React.ReactElement;
|
|
57
|
+
|
|
58
|
+
element.props.onDropdownVisibleChange(true);
|
|
59
|
+
expect(update).toHaveBeenCalledWith({ preventClose: true });
|
|
60
|
+
expect(onDropdownVisibleChange).toHaveBeenCalledWith(true);
|
|
61
|
+
|
|
62
|
+
element.props.onDropdownVisibleChange(false);
|
|
63
|
+
expect(onDropdownVisibleChange).toHaveBeenCalledWith(false);
|
|
64
|
+
expect(update).toHaveBeenCalledTimes(1);
|
|
65
|
+
vi.runAllTimers();
|
|
66
|
+
expect(update).toHaveBeenLastCalledWith({ preventClose: false });
|
|
67
|
+
} finally {
|
|
68
|
+
vi.useRealTimers();
|
|
69
|
+
}
|
|
70
|
+
});
|
|
43
71
|
});
|