@nocobase/client-v2 2.3.0-beta.12 → 2.3.0-beta.14
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/flow/components/filter/VariableFilterItem.d.ts +3 -1
- package/es/flow/internal/utils/quickEditDataScope.d.ts +48 -0
- package/es/flow/models/blocks/form/QuickEditFormModel.d.ts +1 -0
- package/es/index.mjs +61 -61
- package/lib/index.js +61 -61
- 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 +13 -0
- package/src/flow/components/filter/VariableFilterItem.tsx +3 -1
- package/src/flow/internal/utils/__tests__/quickEditDataScope.test.ts +156 -0
- package/src/flow/internal/utils/quickEditDataScope.tsx +177 -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 +30 -11
- package/src/flow/models/blocks/form/__tests__/QuickEditFormModel.quickEdit.test.ts +76 -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/RecordSelectFieldModel.tsx +7 -0
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +5 -0
- package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordSelectFieldModel.quickCreatePopup.test.tsx +183 -0
|
@@ -9,7 +9,10 @@
|
|
|
9
9
|
|
|
10
10
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
11
11
|
import { FlowEngine, FlowModel, SingleRecordResource } from '@nocobase/flow-engine';
|
|
12
|
+
import '../../../index';
|
|
12
13
|
import { QuickEditFormModel } from '../QuickEditFormModel';
|
|
14
|
+
import { dataScope } from '../../../../actions/dataScope';
|
|
15
|
+
import { RecordSelectFieldModel } from '../../../fields/AssociationFieldModel/RecordSelectFieldModel';
|
|
13
16
|
import { SelectFieldModel } from '../../../fields/SelectFieldModel';
|
|
14
17
|
|
|
15
18
|
describe('QuickEditFormModel - quick edit save triggers API (regression)', () => {
|
|
@@ -56,6 +59,54 @@ describe('QuickEditFormModel - quick edit save triggers API (regression)', () =>
|
|
|
56
59
|
});
|
|
57
60
|
};
|
|
58
61
|
|
|
62
|
+
const addMemberCollections = () => {
|
|
63
|
+
const ds = engine.context.dataSourceManager.getDataSource('main');
|
|
64
|
+
ds.addCollection({
|
|
65
|
+
name: 'departments',
|
|
66
|
+
filterTargetKey: 'id',
|
|
67
|
+
titleField: 'name',
|
|
68
|
+
fields: [
|
|
69
|
+
{ name: 'id', type: 'integer', interface: 'number' },
|
|
70
|
+
{ name: 'name', type: 'string', interface: 'input', uiSchema: { title: 'Name' } },
|
|
71
|
+
{ name: 'level', type: 'string', interface: 'input', uiSchema: { title: 'Level' } },
|
|
72
|
+
],
|
|
73
|
+
});
|
|
74
|
+
ds.addCollection({
|
|
75
|
+
name: 'members',
|
|
76
|
+
filterTargetKey: 'id',
|
|
77
|
+
fields: [
|
|
78
|
+
{ name: 'id', type: 'integer', interface: 'number' },
|
|
79
|
+
{ name: 'level', type: 'string', interface: 'input', uiSchema: { title: 'Level' } },
|
|
80
|
+
{
|
|
81
|
+
name: 'department',
|
|
82
|
+
type: 'belongsTo',
|
|
83
|
+
interface: 'm2o',
|
|
84
|
+
target: 'departments',
|
|
85
|
+
targetKey: 'id',
|
|
86
|
+
uiSchema: { title: 'Department' },
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
});
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const openQuickEditForAssociation = async (dataScopeFilter?: Record<string, unknown>) => {
|
|
93
|
+
engine.registerModels({ QuickEditFormModel, RecordSelectFieldModel });
|
|
94
|
+
engine.registerActions({ dataScope });
|
|
95
|
+
addMemberCollections();
|
|
96
|
+
const model = engine.createModel<QuickEditFormModel>({
|
|
97
|
+
use: QuickEditFormModel,
|
|
98
|
+
uid: `qe-association-${dataScopeFilter ? 'scoped' : 'plain'}`,
|
|
99
|
+
stepParams: {
|
|
100
|
+
quickEditFormSettings: {
|
|
101
|
+
init: { dataSourceKey: 'main', collectionName: 'members', fieldPath: 'department' },
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
model._dataScopeFilter = dataScopeFilter;
|
|
106
|
+
await model.applyFlow('quickEditFormSettings', { filterByTk: 7, record: { id: 7, level: 'senior' } });
|
|
107
|
+
return model;
|
|
108
|
+
};
|
|
109
|
+
|
|
59
110
|
it('uses source field context when opening quick edit', async () => {
|
|
60
111
|
engine.registerModels({ QuickEditFormModel });
|
|
61
112
|
engine.context.defineProperty('pageActive', { value: { value: false } });
|
|
@@ -458,4 +509,29 @@ describe('QuickEditFormModel - quick edit save triggers API (regression)', () =>
|
|
|
458
509
|
await mobileModel.applyFlow('quickEditFormSettings', { record: {} });
|
|
459
510
|
expect(mobileModel.subModels.fields[0].props.placement).toBeUndefined();
|
|
460
511
|
});
|
|
512
|
+
|
|
513
|
+
it('hands the column data scope to the association editor built for the popover', async () => {
|
|
514
|
+
const filter = { logic: '$and', items: [{ path: 'level', operator: 'eq', value: '{{ctx.record.level}}' }] };
|
|
515
|
+
|
|
516
|
+
const model = await openQuickEditForAssociation(filter);
|
|
517
|
+
|
|
518
|
+
const fieldModel = model.subModels.fields[0];
|
|
519
|
+
expect(fieldModel.use).toBe('RecordSelectFieldModel');
|
|
520
|
+
expect(fieldModel.getStepParams('selectSettings', 'dataScope')).toEqual({ filter });
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
it('leaves the association editor unscoped when the column has no data scope', async () => {
|
|
524
|
+
const model = await openQuickEditForAssociation();
|
|
525
|
+
|
|
526
|
+
expect(model.subModels.fields[0].getStepParams('selectSettings', 'dataScope')).toBeUndefined();
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
it('seeds the edited row before the editor flows run so record variables resolve', async () => {
|
|
530
|
+
const filter = { logic: '$and', items: [{ path: 'level', operator: 'eq', value: '{{ctx.record.level}}' }] };
|
|
531
|
+
|
|
532
|
+
const model = await openQuickEditForAssociation(filter);
|
|
533
|
+
|
|
534
|
+
expect(model.resource.getData()).toMatchObject({ id: 7, level: 'senior' });
|
|
535
|
+
expect(JSON.stringify(model.subModels.fields[0].resource.getFilter() ?? null)).toContain('senior');
|
|
536
|
+
});
|
|
461
537
|
});
|
|
@@ -152,6 +152,7 @@ const JSBlockPlainHost = ({
|
|
|
152
152
|
afterContent,
|
|
153
153
|
contentRef,
|
|
154
154
|
marginBlock,
|
|
155
|
+
minHeight,
|
|
155
156
|
...rest
|
|
156
157
|
}: React.HTMLAttributes<HTMLDivElement> & {
|
|
157
158
|
uid: string;
|
|
@@ -161,6 +162,7 @@ const JSBlockPlainHost = ({
|
|
|
161
162
|
afterContent?: React.ReactNode;
|
|
162
163
|
contentRef: React.RefObject<HTMLDivElement>;
|
|
163
164
|
marginBlock: number;
|
|
165
|
+
minHeight: number;
|
|
164
166
|
}) => {
|
|
165
167
|
const hostRef = React.useRef<HTMLDivElement | null>(null);
|
|
166
168
|
const resolvedHeight = usePlainHostHeight({ height, heightMode, hostRef, marginBlock });
|
|
@@ -175,7 +177,7 @@ const JSBlockPlainHost = ({
|
|
|
175
177
|
display: 'flex',
|
|
176
178
|
flexDirection: 'column',
|
|
177
179
|
height: resolvedHeight ?? undefined,
|
|
178
|
-
minHeight
|
|
180
|
+
minHeight,
|
|
179
181
|
overflow: 'auto',
|
|
180
182
|
...(style || {}),
|
|
181
183
|
}}
|
|
@@ -228,6 +230,8 @@ export class JSBlockModel extends BlockModel {
|
|
|
228
230
|
afterContent={afterContent}
|
|
229
231
|
contentRef={this.context.ref}
|
|
230
232
|
marginBlock={this.context.themeToken?.marginBlock ?? 0}
|
|
233
|
+
// Keep empty blocks reachable by the settings overlay while the UI editor is active.
|
|
234
|
+
minHeight={this.context.flowSettingsEnabled ? this.context.themeToken?.controlHeightLG ?? 40 : 0}
|
|
231
235
|
/>
|
|
232
236
|
);
|
|
233
237
|
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import React from 'react';
|
|
11
11
|
import { App, ConfigProvider } from 'antd';
|
|
12
|
-
import { render } from '@nocobase/test/client';
|
|
12
|
+
import { act, render, waitFor } from '@nocobase/test/client';
|
|
13
13
|
import { describe, expect, it, vi } from 'vitest';
|
|
14
14
|
import { FlowEngine, FlowEngineProvider } from '@nocobase/flow-engine';
|
|
15
15
|
import { JSBlockModel } from '../JSBlock';
|
|
@@ -78,6 +78,28 @@ describe('JSBlockModel', () => {
|
|
|
78
78
|
expect(model.context.ref.current).toBe(host?.firstElementChild);
|
|
79
79
|
});
|
|
80
80
|
|
|
81
|
+
it('keeps an empty cardless block reachable only while the UI editor is active', async () => {
|
|
82
|
+
const { engine, model } = createJSBlock('js-block-empty-without-card', false, { style: undefined });
|
|
83
|
+
await engine.flowSettings.disable();
|
|
84
|
+
const { container } = renderBlock(engine, model);
|
|
85
|
+
const host = container.querySelector('#model-js-block-empty-without-card') as HTMLElement;
|
|
86
|
+
const content = model.context.ref.current;
|
|
87
|
+
|
|
88
|
+
expect(host.style.minHeight).toBe('0');
|
|
89
|
+
await act(async () => {
|
|
90
|
+
await engine.flowSettings.enable();
|
|
91
|
+
});
|
|
92
|
+
await waitFor(() => expect(host.style.minHeight).toBe('40px'));
|
|
93
|
+
expect(model.context.ref.current).toBe(content);
|
|
94
|
+
expect(container.querySelector('.ant-card')).toBeNull();
|
|
95
|
+
|
|
96
|
+
await act(async () => {
|
|
97
|
+
await engine.flowSettings.disable();
|
|
98
|
+
});
|
|
99
|
+
await waitFor(() => expect(host.style.minHeight).toBe('0'));
|
|
100
|
+
expect(model.context.ref.current).toBe(content);
|
|
101
|
+
});
|
|
102
|
+
|
|
81
103
|
it('keeps cardless specified-height content scrollable inside the plain host', () => {
|
|
82
104
|
const { engine, model } = createJSBlock('js-block-without-card-height', false, {
|
|
83
105
|
heightMode: 'specifyValue',
|
|
@@ -37,6 +37,7 @@ import { getRowKey } from './utils';
|
|
|
37
37
|
import { getSavedAssociationTitleField, getTableColumnSortField } from './sortUtils';
|
|
38
38
|
import { getFieldBindingUse, rebuildFieldSubModel } from '../../../internal/utils/rebuildFieldSubModel';
|
|
39
39
|
import { getSavedDateTimeFormatParams, resolveDateTimeDisplayProps } from '../../../utils/dateTimeDisplayProps';
|
|
40
|
+
import { QuickEditDataScopeInput, resolveQuickEditDataScopeStep } from '../../../internal/utils/quickEditDataScope';
|
|
40
41
|
|
|
41
42
|
export function FieldDeletePlaceholder(props: any) {
|
|
42
43
|
const { t } = useTranslation();
|
|
@@ -542,6 +543,36 @@ TableColumnModel.registerFlow({
|
|
|
542
543
|
ctx.model.setProps('editable', ctx.model.associationPathName ? false : params.editable);
|
|
543
544
|
},
|
|
544
545
|
},
|
|
546
|
+
quickEditDataScope: {
|
|
547
|
+
title: tExpr('Data scope'),
|
|
548
|
+
uiMode: {
|
|
549
|
+
type: 'dialog',
|
|
550
|
+
props: {
|
|
551
|
+
width: 800,
|
|
552
|
+
},
|
|
553
|
+
},
|
|
554
|
+
uiSchema: {
|
|
555
|
+
filter: {
|
|
556
|
+
type: 'object',
|
|
557
|
+
'x-decorator': 'FormItem',
|
|
558
|
+
'x-component': QuickEditDataScopeInput,
|
|
559
|
+
},
|
|
560
|
+
},
|
|
561
|
+
useRawParams: true,
|
|
562
|
+
defaultParams: {
|
|
563
|
+
filter: { logic: '$and', items: [] },
|
|
564
|
+
},
|
|
565
|
+
hideInSettings(ctx) {
|
|
566
|
+
if (ctx.model.associationPathName || !ctx.model.props.editable) {
|
|
567
|
+
return true;
|
|
568
|
+
}
|
|
569
|
+
return !resolveQuickEditDataScopeStep(ctx, ctx.collectionField);
|
|
570
|
+
},
|
|
571
|
+
handler() {
|
|
572
|
+
// Settings-only step. The filter is kept raw on the column and handed to the editor model that
|
|
573
|
+
// QuickEditFormModel builds when the popover opens, so there is nothing to apply to the column itself.
|
|
574
|
+
},
|
|
575
|
+
},
|
|
545
576
|
model: {
|
|
546
577
|
title: tExpr('Field component'),
|
|
547
578
|
use: 'displayFieldComponent',
|
|
@@ -9,8 +9,36 @@
|
|
|
9
9
|
|
|
10
10
|
import { FlowEngine } from '@nocobase/flow-engine';
|
|
11
11
|
import { describe, expect, it, vi } from 'vitest';
|
|
12
|
+
// Prime the model module graph so the editable field bindings used by the quick edit data scope setting are registered.
|
|
13
|
+
import '../../../index';
|
|
14
|
+
import { RecordSelectFieldModel } from '../../../fields/AssociationFieldModel/RecordSelectFieldModel';
|
|
12
15
|
import { TableColumnModel } from '../TableColumnModel';
|
|
13
16
|
|
|
17
|
+
const createQuickEditDataScopeCtx = (
|
|
18
|
+
overrides: { associationPathName?: string; editable?: boolean; isAssociation?: boolean } = {},
|
|
19
|
+
) => {
|
|
20
|
+
const engine = new FlowEngine();
|
|
21
|
+
engine.registerModels({ RecordSelectFieldModel });
|
|
22
|
+
const { associationPathName, editable = true, isAssociation = true } = overrides;
|
|
23
|
+
return {
|
|
24
|
+
engine,
|
|
25
|
+
model: {
|
|
26
|
+
associationPathName,
|
|
27
|
+
props: { editable },
|
|
28
|
+
},
|
|
29
|
+
collectionField: {
|
|
30
|
+
interface: isAssociation ? 'm2o' : 'input',
|
|
31
|
+
isAssociationField: () => isAssociation,
|
|
32
|
+
targetCollection: isAssociation ? { template: 'general' } : undefined,
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const getQuickEditDataScopeStep = (engine: FlowEngine) => {
|
|
38
|
+
const model = new TableColumnModel({ uid: 'table-column-quick-edit-data-scope', flowEngine: engine } as any);
|
|
39
|
+
return model.getFlow('tableColumnSettings')?.steps?.quickEditDataScope as any;
|
|
40
|
+
};
|
|
41
|
+
|
|
14
42
|
describe('TableColumnModel sorter settings', () => {
|
|
15
43
|
it('clamps custom column width to the minimum value', () => {
|
|
16
44
|
const engine = new FlowEngine();
|
|
@@ -30,6 +58,42 @@ describe('TableColumnModel sorter settings', () => {
|
|
|
30
58
|
expect(setProps).toHaveBeenCalledWith('width', 10);
|
|
31
59
|
});
|
|
32
60
|
|
|
61
|
+
it('offers the quick edit data scope on an editable association column', async () => {
|
|
62
|
+
const engine = new FlowEngine();
|
|
63
|
+
const step = getQuickEditDataScopeStep(engine);
|
|
64
|
+
|
|
65
|
+
const hidden = await step.hideInSettings(createQuickEditDataScopeCtx());
|
|
66
|
+
|
|
67
|
+
expect(hidden).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('hides the quick edit data scope when quick edit is off', async () => {
|
|
71
|
+
const engine = new FlowEngine();
|
|
72
|
+
const step = getQuickEditDataScopeStep(engine);
|
|
73
|
+
|
|
74
|
+
const hidden = await step.hideInSettings(createQuickEditDataScopeCtx({ editable: false }));
|
|
75
|
+
|
|
76
|
+
expect(hidden).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('hides the quick edit data scope for non-association columns', async () => {
|
|
80
|
+
const engine = new FlowEngine();
|
|
81
|
+
const step = getQuickEditDataScopeStep(engine);
|
|
82
|
+
|
|
83
|
+
const hidden = await step.hideInSettings(createQuickEditDataScopeCtx({ isAssociation: false }));
|
|
84
|
+
|
|
85
|
+
expect(hidden).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('hides the quick edit data scope for relation path columns', async () => {
|
|
89
|
+
const engine = new FlowEngine();
|
|
90
|
+
const step = getQuickEditDataScopeStep(engine);
|
|
91
|
+
|
|
92
|
+
const hidden = await step.hideInSettings(createQuickEditDataScopeCtx({ associationPathName: 'department' }));
|
|
93
|
+
|
|
94
|
+
expect(hidden).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
|
|
33
97
|
it('hides quick edit setting for relation path columns added from association groups', async () => {
|
|
34
98
|
const engine = new FlowEngine();
|
|
35
99
|
const model = new TableColumnModel({ uid: 'table-column-relation-path-quick-edit', flowEngine: engine } as any);
|
|
@@ -66,6 +66,11 @@ function RemoteModelRenderer({ options }) {
|
|
|
66
66
|
|
|
67
67
|
export function CreateContent({ model, toOne = false }) {
|
|
68
68
|
const ctx = useFlowContext();
|
|
69
|
+
// Quick-create popups isolate business context but must retain the field's configuration mode.
|
|
70
|
+
ctx.defineProperty('flowSettingsEnabled', {
|
|
71
|
+
get: () => model.context.flowSettingsEnabled,
|
|
72
|
+
cache: false,
|
|
73
|
+
});
|
|
69
74
|
const { Header, type } = ctx.view;
|
|
70
75
|
model._closeView = ctx.view.close;
|
|
71
76
|
return (
|
|
@@ -876,6 +881,8 @@ RecordSelectFieldModel.registerFlow({
|
|
|
876
881
|
dataSourceKey: ctx.collection.dataSourceKey,
|
|
877
882
|
collectionName: ctx.collectionField?.target,
|
|
878
883
|
...(associationName && sourceId != null ? { associationName, sourceId } : {}),
|
|
884
|
+
// 上级记录暂无主键(如审批配置态表单)时不能传 associationName,否则弹窗内新建的区块会绑定到关联资源;改传 sourceAssociationName,仅用于展示“当前弹窗上级记录”变量
|
|
885
|
+
...(associationName && sourceId == null ? { sourceAssociationName: associationName } : {}),
|
|
879
886
|
collectionField: ctx.collectionField,
|
|
880
887
|
openerUids,
|
|
881
888
|
onChange: (e) => {
|
package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx
CHANGED
|
@@ -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;
|
|
@@ -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
|
+
});
|