@nocobase/client-v2 2.3.0-beta.9 → 2.4.0-alpha.10
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/RouteRepository.d.ts +6 -0
- package/es/acl/ACLProvider.d.ts +10 -0
- package/es/acl/aclCheckReadiness.d.ts +11 -0
- package/es/authRedirect.d.ts +11 -2
- package/es/components/AttachmentUpload.d.ts +51 -0
- package/es/components/index.d.ts +1 -0
- package/es/flow/admin-shell/admin-layout/NocoBaseLogo.d.ts +10 -0
- package/es/flow/components/filter/VariableFilterItem.d.ts +10 -1
- package/es/flow/internal/utils/quickEditDataScope.d.ts +48 -0
- package/es/flow/internal/utils/quickEditViewContainer.d.ts +27 -0
- package/es/flow/models/blocks/form/QuickEditFormModel.d.ts +1 -0
- package/es/index.d.ts +4 -1
- package/es/index.mjs +129 -118
- package/es/nocobase-buildin-plugin/currentUserAuthStatus.d.ts +11 -0
- package/es/nocobase-buildin-plugin/index.d.ts +10 -0
- package/es/settings-app/SettingsApplication.d.ts +18 -0
- package/es/settings-app/SettingsBrand.d.ts +16 -0
- package/es/settings-app/SettingsBuildInPlugin.d.ts +9 -0
- package/es/settings-app/SettingsDocumentRedirect.d.ts +10 -0
- package/es/settings-app/SettingsGroupNav.d.ts +18 -0
- package/es/settings-app/SettingsPluginSettingsManager.d.ts +14 -0
- package/es/settings-app/SettingsRouterManager.d.ts +15 -0
- package/es/settings-app/SettingsSearch.d.ts +17 -0
- package/es/settings-app/SettingsShell.d.ts +10 -0
- package/es/settings-app/isSettingsApp.d.ts +18 -0
- package/es/settings-app/runtimePaths.d.ts +18 -0
- package/es/settings-app/settingsDocumentPath.d.ts +14 -0
- package/es/settings-app/settingsTheme.d.ts +87 -0
- package/es/settings-app/useSettingsThemeConfig.d.ts +22 -0
- package/es/settings-center/AdminSettingsLayout.d.ts +2 -2
- package/es/settings-center/groups.d.ts +66 -0
- package/es/settings-center/useSettingsGroups.d.ts +36 -0
- package/es/settings-center/useSettingsSearch.d.ts +35 -0
- package/es/settings-center/utils.d.ts +24 -0
- package/es/theme/index.d.ts +1 -0
- package/es/theme/itemActive.d.ts +24 -0
- package/es/theme/type.d.ts +9 -0
- package/lib/index.js +133 -122
- package/package.json +7 -7
- package/src/BaseApplication.tsx +2 -0
- package/src/RouteRepository.ts +14 -0
- package/src/__tests__/RouteRepository.test.ts +51 -0
- package/src/__tests__/app.test.tsx +135 -3
- package/src/__tests__/authRedirect.test.ts +154 -0
- package/src/__tests__/browserChecker.test.ts +172 -6
- package/src/__tests__/mockSettingsApplication.ts +29 -0
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +81 -8
- package/src/__tests__/plugin-manager.test.tsx +7 -6
- package/src/__tests__/settings-application.test.ts +164 -0
- package/src/__tests__/settings-auth-routes.test.ts +71 -0
- package/src/__tests__/settings-center.test.tsx +168 -130
- package/src/__tests__/settings-layout-root.test.tsx +600 -0
- package/src/__tests__/settings-runtime-paths.test.ts +99 -0
- package/src/__tests__/settings-search-shortcut.test.tsx +321 -0
- package/src/__tests__/settings-shell.test.tsx +261 -0
- package/src/acl/ACLProvider.tsx +53 -3
- package/src/acl/aclCheckReadiness.ts +44 -0
- package/src/authRedirect.ts +85 -8
- package/src/components/AppComponents.tsx +16 -7
- package/src/components/AttachmentUpload.tsx +275 -0
- package/src/components/index.ts +1 -0
- package/src/flow/actions/__tests__/validation.test.ts +48 -0
- package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +1 -56
- package/src/flow/admin-shell/admin-layout/NocoBaseLogo.tsx +77 -0
- package/src/flow/admin-shell/admin-layout/TopbarActionsBar.tsx +37 -14
- package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +104 -20
- package/src/flow/components/filter/VariableFilterItem.tsx +14 -4
- package/src/flow/components/filter/__tests__/VariableFilterItem.rightMetaTree.test.tsx +158 -0
- package/src/flow/internal/utils/__tests__/quickEditDataScope.test.ts +156 -0
- package/src/flow/internal/utils/__tests__/quickEditViewContainer.test.ts +52 -0
- package/src/flow/internal/utils/quickEditDataScope.tsx +177 -0
- package/src/flow/internal/utils/quickEditViewContainer.ts +46 -0
- package/src/flow/models/actions/JSItemActionModel.tsx +19 -3
- package/src/flow/models/actions/__tests__/JSItemActionModel.test.tsx +47 -1
- package/src/flow/models/blocks/form/QuickEditFormModel.tsx +45 -11
- package/src/flow/models/blocks/form/__tests__/QuickEditFormModel.quickEdit.test.ts +120 -0
- package/src/flow/models/blocks/js-block/JSBlock.tsx +5 -1
- package/src/flow/models/blocks/js-block/__tests__/JSBlockModel.test.tsx +23 -1
- package/src/flow/models/blocks/table/TableColumnModel.tsx +31 -0
- package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +64 -0
- package/src/flow/models/fields/AssociationFieldModel/CascadeSelectFieldModel.tsx +7 -1
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +4 -3
- package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +9 -0
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +17 -10
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +6 -1
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableField.disabled.test.tsx +90 -0
- package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordSelectFieldModel.quickCreatePopup.test.tsx +183 -0
- package/src/flow/models/fields/SelectFieldModel.tsx +5 -0
- package/src/flow/models/fields/__tests__/SelectFieldModel.test.tsx +29 -1
- package/src/flow/models/topbar/TopbarActionModel.tsx +52 -180
- package/src/index.ts +11 -1
- package/src/nocobase-buildin-plugin/currentUserAuthStatus.ts +58 -0
- package/src/nocobase-buildin-plugin/index.tsx +112 -71
- package/src/settings-app/SettingsApplication.ts +31 -0
- package/src/settings-app/SettingsBrand.tsx +108 -0
- package/src/settings-app/SettingsBuildInPlugin.ts +10 -0
- package/src/settings-app/SettingsDocumentRedirect.tsx +26 -0
- package/src/settings-app/SettingsGroupNav.tsx +124 -0
- package/src/settings-app/SettingsPluginSettingsManager.ts +38 -0
- package/src/settings-app/SettingsRouterManager.ts +92 -0
- package/src/settings-app/SettingsSearch.tsx +343 -0
- package/src/settings-app/SettingsShell.tsx +184 -0
- package/src/settings-app/isSettingsApp.ts +21 -0
- package/src/settings-app/runtimePaths.ts +103 -0
- package/src/settings-app/settingsDocumentPath.ts +66 -0
- package/src/settings-app/settingsTheme.ts +369 -0
- package/src/settings-app/useSettingsThemeConfig.ts +100 -0
- package/src/settings-center/AdminSettingsLayout.tsx +154 -169
- package/src/settings-center/SystemSettingsPage.tsx +4 -170
- package/src/settings-center/__tests__/groups.test.ts +25 -0
- package/src/settings-center/groups.ts +108 -0
- package/src/settings-center/useSettingsGroups.ts +105 -0
- package/src/settings-center/useSettingsSearch.ts +173 -0
- package/src/settings-center/utils.tsx +50 -1
- package/src/theme/index.tsx +1 -0
- package/src/theme/itemActive.ts +31 -0
- package/src/theme/type.ts +10 -0
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
|
);
|
|
@@ -620,6 +620,11 @@ export class SubTableColumnModel<
|
|
|
620
620
|
|
|
621
621
|
onInit(options: any): void {
|
|
622
622
|
super.onInit(options);
|
|
623
|
+
const resource = this.context.blockModel?.context?.resource;
|
|
624
|
+
const fieldPath = this.context.fieldPath;
|
|
625
|
+
if (resource?.addUpdateAssociationValues && fieldPath && this.collectionField?.isAssociationField?.()) {
|
|
626
|
+
resource.addUpdateAssociationValues(fieldPath);
|
|
627
|
+
}
|
|
623
628
|
this.context.defineProperty('resourceName', {
|
|
624
629
|
get: () => {
|
|
625
630
|
return this.context.collectionField.collection.name;
|
|
@@ -740,7 +745,7 @@ export class SubTableColumnModel<
|
|
|
740
745
|
}
|
|
741
746
|
renderItem(): any {
|
|
742
747
|
return (props) => {
|
|
743
|
-
const { value, id, rowIdx, record, parentFieldIndex, parentItem } = props || {};
|
|
748
|
+
const { value, id, rowIdx, record, parentFieldIndex, parentItem, disabled: subTableDisabled } = props || {};
|
|
744
749
|
// 子表格列模型本身没有行级 fieldIndex,上下文中无法把 `roles.name` 解析成 `roles[0].name`,
|
|
745
750
|
// 导致“默认值/赋值规则”在对多关系字段下无法生效。
|
|
746
751
|
// 这里为每一行创建一个 column fork,并注入 fieldIndex,让规则引擎能够按行解析与写入。
|
|
@@ -754,6 +759,7 @@ export class SubTableColumnModel<
|
|
|
754
759
|
const fieldModel: any = this.subModels.field;
|
|
755
760
|
const cellModeKey = [
|
|
756
761
|
rowForkKey,
|
|
762
|
+
subTableDisabled ? 'disabled' : 'enabled',
|
|
757
763
|
this.props.pattern,
|
|
758
764
|
this.props.readPretty,
|
|
759
765
|
this.props.titleField,
|
|
@@ -828,6 +834,7 @@ export class SubTableColumnModel<
|
|
|
828
834
|
parentItem={parentItem}
|
|
829
835
|
rowFork={rowFork}
|
|
830
836
|
memoKey={cellModeKey}
|
|
837
|
+
disabled={subTableDisabled}
|
|
831
838
|
width={this.props.width}
|
|
832
839
|
commitOnChange={this.hasFormulaColumn || this.hasFormValueDrivenDataScopeColumn}
|
|
833
840
|
/>
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { CloseOutlined, ZoomInOutlined } from '@ant-design/icons';
|
|
11
|
-
import { Table, Form, Space, Button } from 'antd';
|
|
11
|
+
import { Table, Form, Space, Button, theme } from 'antd';
|
|
12
12
|
import { css } from '@emotion/css';
|
|
13
13
|
import { useTranslation } from 'react-i18next';
|
|
14
14
|
import { PlusOutlined } from '@ant-design/icons';
|
|
@@ -51,6 +51,7 @@ function shouldRefreshForChangedPaths(fieldPath: unknown, changedPaths: unknown)
|
|
|
51
51
|
|
|
52
52
|
export function SubTableField(props) {
|
|
53
53
|
const { t } = useTranslation();
|
|
54
|
+
const { token } = theme.useToken();
|
|
54
55
|
const {
|
|
55
56
|
onChange,
|
|
56
57
|
columns,
|
|
@@ -180,6 +181,9 @@ export function SubTableField(props) {
|
|
|
180
181
|
// the current array index so Form.Item can rebind after reordering/removal.
|
|
181
182
|
const rowBindingKey = `${rowIdentity}:${pageRowIdx}`;
|
|
182
183
|
const columnKey = col.dataIndex ?? col.key ?? 'cell';
|
|
184
|
+
if (col.key === '_empty') {
|
|
185
|
+
return <div style={{ minHeight: token.controlHeight }} />;
|
|
186
|
+
}
|
|
183
187
|
if (!col.render) {
|
|
184
188
|
return;
|
|
185
189
|
}
|
|
@@ -188,6 +192,7 @@ export function SubTableField(props) {
|
|
|
188
192
|
rowIdx: pageRowIdx,
|
|
189
193
|
id: `field-${String(columnKey)}-${rowBindingKey}`,
|
|
190
194
|
value: text,
|
|
195
|
+
disabled,
|
|
191
196
|
parentFieldIndex,
|
|
192
197
|
parentItem,
|
|
193
198
|
onChange: (value) => {
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,183 @@
|
|
|
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 {
|
|
11
|
+
FlowContext,
|
|
12
|
+
FlowEngine,
|
|
13
|
+
FlowEngineProvider,
|
|
14
|
+
FlowViewContextProvider,
|
|
15
|
+
type FlowModel,
|
|
16
|
+
} from '@nocobase/flow-engine';
|
|
17
|
+
import { cleanup, render, screen, waitFor } from '@testing-library/react';
|
|
18
|
+
import React from 'react';
|
|
19
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
20
|
+
import '../../../index';
|
|
21
|
+
import { BlockGridModel } from '../../../base/BlockGridModel';
|
|
22
|
+
import { RecordSelectFieldModel } from '../RecordSelectFieldModel';
|
|
23
|
+
|
|
24
|
+
type OpenViewHandler = (ctx: unknown, params: Record<string, unknown>) => void;
|
|
25
|
+
|
|
26
|
+
function getOpenViewHandler(): OpenViewHandler {
|
|
27
|
+
const flow = RecordSelectFieldModel.globalFlowRegistry.getFlow('popupSettings');
|
|
28
|
+
const handler = flow?.getStep('openView')?.serialize().handler;
|
|
29
|
+
|
|
30
|
+
if (!handler) {
|
|
31
|
+
throw new Error('popupSettings.openView handler is not registered');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return handler as OpenViewHandler;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function createContext(record: Record<string, unknown>) {
|
|
38
|
+
const open = vi.fn();
|
|
39
|
+
const sourceCollection = {
|
|
40
|
+
getFilterByTK: vi.fn((sourceRecord: Record<string, unknown>) => sourceRecord.id),
|
|
41
|
+
};
|
|
42
|
+
const model = {
|
|
43
|
+
uid: 'record-select-uid',
|
|
44
|
+
props: {
|
|
45
|
+
quickCreate: 'modalAdd',
|
|
46
|
+
allowMultiple: true,
|
|
47
|
+
},
|
|
48
|
+
context: {
|
|
49
|
+
inputArgs: {},
|
|
50
|
+
flowSettingsEnabled: false,
|
|
51
|
+
},
|
|
52
|
+
flowEngine: {
|
|
53
|
+
context: {
|
|
54
|
+
themeToken: {
|
|
55
|
+
colorBgLayout: '#fff',
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
open,
|
|
63
|
+
context: {
|
|
64
|
+
inputArgs: { onChange: vi.fn() },
|
|
65
|
+
view: {
|
|
66
|
+
inputArgs: {
|
|
67
|
+
viewUid: 'approval-popup-uid',
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
viewer: { open },
|
|
71
|
+
layoutContentElement: {},
|
|
72
|
+
model,
|
|
73
|
+
collection: {
|
|
74
|
+
dataSourceKey: 'main',
|
|
75
|
+
filterTargetKey: 'id',
|
|
76
|
+
},
|
|
77
|
+
collectionField: {
|
|
78
|
+
type: 'hasMany',
|
|
79
|
+
target: 'orgs',
|
|
80
|
+
resourceName: 'users.orgs',
|
|
81
|
+
collection: sourceCollection,
|
|
82
|
+
},
|
|
83
|
+
record,
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
describe('RecordSelectFieldModel quick create popup', () => {
|
|
89
|
+
afterEach(() => {
|
|
90
|
+
cleanup();
|
|
91
|
+
vi.restoreAllMocks();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe.each(['drawer', 'dialog'])('%s configuration mode', (mode) => {
|
|
95
|
+
it.each([
|
|
96
|
+
{ scenario: 'configuration', modelEnabled: true, globalEnabled: false },
|
|
97
|
+
{ scenario: 'global configuration', modelEnabled: true, globalEnabled: true },
|
|
98
|
+
{ scenario: 'runtime', modelEnabled: false, globalEnabled: false },
|
|
99
|
+
{ scenario: 'read-only configuration', modelEnabled: false, globalEnabled: true },
|
|
100
|
+
])('uses the field configuration state in $scenario', async ({ modelEnabled, globalEnabled }) => {
|
|
101
|
+
const engine = new FlowEngine();
|
|
102
|
+
engine.registerModels({ BlockGridModel });
|
|
103
|
+
engine.flowSettings.enabled = globalEnabled;
|
|
104
|
+
engine.context.defineProperty('themeToken', {
|
|
105
|
+
value: { marginBlock: 16, marginSM: 8, paddingLG: 24 },
|
|
106
|
+
});
|
|
107
|
+
engine.context.defineProperty('t', { value: (key: string) => key });
|
|
108
|
+
const save = vi.fn(async (model: FlowModel) => model.serialize());
|
|
109
|
+
engine.setModelRepository({
|
|
110
|
+
findOne: vi.fn(async () => null),
|
|
111
|
+
save,
|
|
112
|
+
destroy: vi.fn(async () => true),
|
|
113
|
+
move: vi.fn(async () => undefined),
|
|
114
|
+
duplicate: vi.fn(async () => null),
|
|
115
|
+
});
|
|
116
|
+
const load = vi.spyOn(engine, 'loadOrCreateModel');
|
|
117
|
+
const { context, open } = createContext({});
|
|
118
|
+
context.model.context.flowSettingsEnabled = modelEnabled;
|
|
119
|
+
getOpenViewHandler()(context, { mode, size: 'medium' });
|
|
120
|
+
const config = open.mock.calls[0][0];
|
|
121
|
+
expect(config.inheritContext).toBe(false);
|
|
122
|
+
|
|
123
|
+
// Detached popups inherit the engine context, not the approval form's model context.
|
|
124
|
+
const popupContext = new FlowContext();
|
|
125
|
+
popupContext.addDelegate(engine.context);
|
|
126
|
+
popupContext.defineProperty('engine', { value: engine });
|
|
127
|
+
popupContext.defineProperty('view', {
|
|
128
|
+
value: { type: mode, inputArgs: config.inputArgs, Header: () => null, close: vi.fn() },
|
|
129
|
+
});
|
|
130
|
+
const { container } = render(
|
|
131
|
+
<FlowEngineProvider engine={engine}>
|
|
132
|
+
<FlowViewContextProvider context={popupContext}>{config.content()}</FlowViewContextProvider>
|
|
133
|
+
</FlowEngineProvider>,
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
await waitFor(() => expect(container.querySelector('.nb-block-grid')).toBeInTheDocument());
|
|
137
|
+
if (modelEnabled) {
|
|
138
|
+
expect(screen.getByRole('button', { name: /Add block/ })).toBeInTheDocument();
|
|
139
|
+
expect(save).toHaveBeenCalledOnce();
|
|
140
|
+
} else {
|
|
141
|
+
expect(screen.queryByRole('button', { name: /Add block/ })).not.toBeInTheDocument();
|
|
142
|
+
expect(save).not.toHaveBeenCalled();
|
|
143
|
+
}
|
|
144
|
+
expect(load).toHaveBeenCalledWith(
|
|
145
|
+
expect.objectContaining({ use: 'BlockGridModel', parentId: context.model.uid }),
|
|
146
|
+
{ delegateToParent: false, delegate: popupContext, skipSave: !modelEnabled },
|
|
147
|
+
);
|
|
148
|
+
expect(engine.context.flowSettingsEnabled).toBe(globalEnabled);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('passes the parent record reference when the parent record has a primary key', () => {
|
|
153
|
+
const { context, open } = createContext({ id: 1, nickname: 'Saved user' });
|
|
154
|
+
|
|
155
|
+
getOpenViewHandler()(context, { mode: 'drawer', size: 'medium' });
|
|
156
|
+
|
|
157
|
+
expect(open).toHaveBeenCalledOnce();
|
|
158
|
+
const { inputArgs } = open.mock.calls[0][0];
|
|
159
|
+
expect(inputArgs).toMatchObject({
|
|
160
|
+
scene: 'create',
|
|
161
|
+
collectionName: 'orgs',
|
|
162
|
+
associationName: 'users.orgs',
|
|
163
|
+
sourceId: 1,
|
|
164
|
+
});
|
|
165
|
+
expect(inputArgs).not.toHaveProperty('sourceAssociationName');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('only passes sourceAssociationName when the parent record has no primary key yet', () => {
|
|
169
|
+
const { context, open } = createContext({});
|
|
170
|
+
|
|
171
|
+
getOpenViewHandler()(context, { mode: 'drawer', size: 'medium' });
|
|
172
|
+
|
|
173
|
+
expect(open).toHaveBeenCalledOnce();
|
|
174
|
+
const { inputArgs } = open.mock.calls[0][0];
|
|
175
|
+
expect(inputArgs).toMatchObject({
|
|
176
|
+
scene: 'create',
|
|
177
|
+
collectionName: 'orgs',
|
|
178
|
+
sourceAssociationName: 'users.orgs',
|
|
179
|
+
});
|
|
180
|
+
expect(inputArgs).not.toHaveProperty('associationName');
|
|
181
|
+
expect(inputArgs).not.toHaveProperty('sourceId');
|
|
182
|
+
});
|
|
183
|
+
});
|
|
@@ -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
|
});
|