@nocobase/client-v2 2.3.0-beta.2 → 2.3.0-beta.21
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/components/form/ScanInput/useCodeScanner.d.ts +12 -2
- package/es/components/form/ScanInput/zxingWasmDecoder.d.ts +9 -0
- package/es/flow/admin-shell/admin-layout/MobileTopbarActionsContext.d.ts +10 -0
- package/es/flow/components/FieldAssignValueInput.d.ts +2 -0
- package/es/flow/components/field-value-variable/FieldValueVariableInput.d.ts +1 -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/actions/LinkActionUtils.d.ts +1 -0
- package/es/flow/models/blocks/assign-form/assignFormValue.d.ts +29 -0
- package/es/flow/models/blocks/form/QuickEditFormModel.d.ts +1 -0
- package/es/index.mjs +100 -100
- package/lib/index.js +102 -102
- package/package.json +9 -8
- 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/collection-manager/__tests__/field-configure.test.ts +52 -0
- package/src/collection-manager/field-configure.ts +2 -2
- package/src/components/form/ScanInput/CodeScanner.tsx +3 -1
- package/src/components/form/ScanInput/__tests__/CodeScanner.test.tsx +7 -1
- package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +182 -8
- package/src/components/form/ScanInput/__tests__/zxingWasmDecoder.test.ts +78 -0
- package/src/components/form/ScanInput/useCodeScanner.ts +135 -20
- package/src/components/form/ScanInput/zxingWasmDecoder.ts +64 -0
- package/src/flow/actions/__tests__/validation.test.ts +48 -0
- package/src/flow/admin-shell/admin-layout/MobileTopbarActionsContext.ts +13 -0
- package/src/flow/admin-shell/admin-layout/TopbarActionsBar.tsx +13 -2
- package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.mobile.test.tsx +205 -0
- package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +42 -23
- package/src/flow/components/FieldAssignValueInput.tsx +4 -0
- package/src/flow/components/field-value-variable/FieldValueVariableInput.tsx +21 -12
- package/src/flow/components/field-value-variable/__tests__/FieldValueVariableInput.test.tsx +9 -0
- 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/components/Markdown/DisplayMarkdown.tsx +14 -9
- package/src/flow/internal/components/Markdown/__tests__/DisplayMarkdown.test.tsx +46 -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/LinkActionModel.tsx +11 -9
- package/src/flow/models/actions/LinkActionUtils.ts +4 -2
- package/src/flow/models/actions/__tests__/JSItemActionModel.test.tsx +47 -1
- package/src/flow/models/actions/__tests__/LinkActionModel.test.ts +14 -0
- package/src/flow/models/base/GridModel.tsx +0 -1
- package/src/flow/models/blocks/assign-form/AssignFormGridModel.tsx +22 -1
- package/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx +40 -12
- package/src/flow/models/blocks/assign-form/__tests__/assignFieldValuesFlow.editor.test.tsx +140 -0
- package/src/flow/models/blocks/assign-form/__tests__/assignFieldValuesFlow.multipleSelect.test.tsx +216 -0
- package/src/flow/models/blocks/assign-form/__tests__/assignFormValue.test.ts +41 -0
- package/src/flow/models/blocks/assign-form/assignFormValue.ts +64 -0
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.toggleFormFieldsCollapse.test.ts +29 -0
- package/src/flow/models/blocks/filter-form/fields/FieldComponentProps.tsx +1 -1
- package/src/flow/models/blocks/filter-form/fields/__tests__/FieldComponentProps.options.test.tsx +61 -0
- package/src/flow/models/blocks/form/FormBlockModel.tsx +26 -5
- package/src/flow/models/blocks/form/QuickEditFormModel.tsx +45 -11
- package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +145 -0
- package/src/flow/models/blocks/form/__tests__/QuickEditFormModel.quickEdit.test.ts +120 -0
- package/src/flow/models/blocks/form/__tests__/popupLinkage.test.tsx +175 -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 +11 -2
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +4 -3
- package/src/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.tsx +6 -3
- package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +13 -1
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +47 -17
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +6 -1
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableColumnModel.largeField.test.tsx +178 -0
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableField.disabled.test.tsx +90 -0
- package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +26 -2
- package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordSelectFieldModel.quickCreatePopup.test.tsx +183 -0
- package/src/flow/models/fields/AssociationFieldModel/__tests__/associationSelect.search.test.ts +113 -0
- package/src/flow/models/fields/DisplayAssociationField/DisplaySubTableFieldModel.tsx +42 -7
- package/src/flow/models/fields/DisplayAssociationField/__tests__/DisplaySubTableFieldModel.test.tsx +351 -0
- package/src/flow/models/fields/DisplayHtmlFieldModel.tsx +2 -1
- package/src/flow/models/fields/InputFieldModel.tsx +1 -1
- package/src/flow/models/fields/RichTextFieldModel/__tests__/RichTextFieldModel.test.tsx +74 -0
- package/src/flow/models/fields/RichTextFieldModel/index.tsx +27 -6
- package/src/flow/models/fields/SelectFieldModel.tsx +5 -0
- package/src/flow/models/fields/__tests__/InputFieldModel.test.tsx +16 -0
- package/src/flow/models/fields/__tests__/SelectFieldModel.test.tsx +29 -1
- package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +1 -0
- package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +20 -0
- package/src/flow/models/topbar/TopbarActionModel.tsx +5 -5
- package/src/flow/models/topbar/UserCenterTopbarActionModel.tsx +8 -4
|
@@ -0,0 +1,175 @@
|
|
|
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 { act, render, screen, waitFor, cleanup } from '@testing-library/react';
|
|
12
|
+
import { Form, Input } from 'antd';
|
|
13
|
+
import { afterEach, expect, it, vi } from 'vitest';
|
|
14
|
+
import { FlowEngine, SingleRecordResource } from '@nocobase/flow-engine';
|
|
15
|
+
import { generateFlowModelRdFromToken } from '@nocobase/utils/client';
|
|
16
|
+
import { CreateFormModel, EditFormModel, FormGridModel, FormItemModel, FormComponent } from '../../../..';
|
|
17
|
+
import { fieldLinkageRules, linkageAssignField } from '../../../../actions/linkageRules';
|
|
18
|
+
|
|
19
|
+
afterEach(cleanup);
|
|
20
|
+
|
|
21
|
+
it.each([
|
|
22
|
+
{ use: 'CreateFormModel', mode: 'default', expected: 'STAFF-001' },
|
|
23
|
+
{ use: 'EditFormModel', mode: 'default', expected: '' },
|
|
24
|
+
{ use: 'EditFormModel', mode: 'override', expected: 'STAFF-001' },
|
|
25
|
+
])('$use respects $mode linkage after a delayed popup variable response', async ({ use, mode, expected }) => {
|
|
26
|
+
const uid = `popup-${use}-${mode}`;
|
|
27
|
+
const engine = new FlowEngine();
|
|
28
|
+
engine.registerModels({ CreateFormModel, EditFormModel, FormGridModel, FormItemModel });
|
|
29
|
+
engine.registerActions({ fieldLinkageRules, linkageAssignField });
|
|
30
|
+
engine.context.dataSourceManager.getDataSource('main').addCollection({
|
|
31
|
+
name: 't1_user',
|
|
32
|
+
filterTargetKey: 'id',
|
|
33
|
+
fields: [
|
|
34
|
+
{ name: 'id', type: 'integer', interface: 'number' },
|
|
35
|
+
{ name: 'staffseq', type: 'string', interface: 'input' },
|
|
36
|
+
{ name: 'staffname', type: 'string', interface: 'input' },
|
|
37
|
+
],
|
|
38
|
+
});
|
|
39
|
+
const form = engine.createModel<CreateFormModel | EditFormModel>({
|
|
40
|
+
uid,
|
|
41
|
+
use,
|
|
42
|
+
stepParams: { resourceSettings: { init: { dataSourceKey: 'main', collectionName: 't1_user', filterByTk: 1 } } },
|
|
43
|
+
subModels: {
|
|
44
|
+
grid: {
|
|
45
|
+
uid: `${uid}-grid`,
|
|
46
|
+
use: 'FormGridModel',
|
|
47
|
+
subModels: {
|
|
48
|
+
items: [
|
|
49
|
+
{
|
|
50
|
+
uid: `${uid}-staffname`,
|
|
51
|
+
use: 'FormItemModel',
|
|
52
|
+
props: { name: 'staffname' },
|
|
53
|
+
stepParams: {
|
|
54
|
+
fieldSettings: { init: { dataSourceKey: 'main', collectionName: 't1_user', fieldPath: 'staffname' } },
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
const configured = '{{ ctx.popup.record.staffseq }}';
|
|
63
|
+
const rules = {
|
|
64
|
+
value: [
|
|
65
|
+
{
|
|
66
|
+
key: 'rule',
|
|
67
|
+
title: 'Rule',
|
|
68
|
+
enable: true,
|
|
69
|
+
condition: { logic: '$and', items: [] },
|
|
70
|
+
actions: [
|
|
71
|
+
{
|
|
72
|
+
key: 'assign',
|
|
73
|
+
name: 'linkageAssignField',
|
|
74
|
+
params: {
|
|
75
|
+
value: [
|
|
76
|
+
{
|
|
77
|
+
key: 'field',
|
|
78
|
+
enable: true,
|
|
79
|
+
mode,
|
|
80
|
+
condition: { logic: '$and', items: [] },
|
|
81
|
+
targetPath: 'staffname',
|
|
82
|
+
value: configured,
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
};
|
|
91
|
+
form.setStepParams('eventSettings', 'linkageRules', rules);
|
|
92
|
+
const saved = JSON.parse(JSON.stringify(form.serialize()));
|
|
93
|
+
expect(saved.subModels.grid.stepParams.eventSettings.linkageRules).toEqual(rules);
|
|
94
|
+
expect(saved.stepParams.eventSettings?.linkageRules).toBeUndefined();
|
|
95
|
+
|
|
96
|
+
const token = `test.${Buffer.from(JSON.stringify({ userId: 2, signInTime: 'member-form' })).toString('base64url')}.sig`;
|
|
97
|
+
type Item = { id: string; rd: string; template: unknown; contextParams: unknown };
|
|
98
|
+
let resolveResponse: (() => void) | undefined;
|
|
99
|
+
const responseReady = new Promise<void>((resolve) => {
|
|
100
|
+
resolveResponse = resolve;
|
|
101
|
+
});
|
|
102
|
+
const request = vi.fn(async ({ data }: { data: { values: { batch: Item[] } } }) => {
|
|
103
|
+
const batch = data.values.batch;
|
|
104
|
+
expect(batch[0].rd).toBe(generateFlowModelRdFromToken(form.uid, token));
|
|
105
|
+
expect(batch[0].contextParams).toEqual({
|
|
106
|
+
'popup.record': { collection: 't1_user', dataSourceKey: 'main', filterByTk: '1' },
|
|
107
|
+
});
|
|
108
|
+
await responseReady;
|
|
109
|
+
return {
|
|
110
|
+
data: {
|
|
111
|
+
data: {
|
|
112
|
+
results: batch.map((item) => ({
|
|
113
|
+
id: item.id,
|
|
114
|
+
data: JSON.parse(JSON.stringify(item.template).replaceAll(configured, 'STAFF-001')),
|
|
115
|
+
})),
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
});
|
|
120
|
+
engine.context.defineProperty('api', { value: { auth: { token, role: 'member' }, request } });
|
|
121
|
+
form.context.defineProperty('popup', {
|
|
122
|
+
value: { record: { id: 1 } },
|
|
123
|
+
resolveOnServer: true,
|
|
124
|
+
meta: {
|
|
125
|
+
type: 'object',
|
|
126
|
+
buildVariablesParams: () => ({ record: { collection: 't1_user', dataSourceKey: 'main', filterByTk: '1' } }),
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
const resource = form.resource as SingleRecordResource;
|
|
130
|
+
resource.setData(use === 'EditFormModel' ? { id: 1, staffname: null } : {});
|
|
131
|
+
function View() {
|
|
132
|
+
form.useHooksBeforeRender();
|
|
133
|
+
return (
|
|
134
|
+
<FormComponent model={form}>
|
|
135
|
+
<Form.Item name="staffname">
|
|
136
|
+
<Input aria-label="staffname" />
|
|
137
|
+
</Form.Item>
|
|
138
|
+
</FormComponent>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
const dispatchEvent = vi.spyOn(form, 'dispatchEvent');
|
|
142
|
+
const view = render(<View />);
|
|
143
|
+
try {
|
|
144
|
+
form.formValueRuntime?.mount({ sync: true });
|
|
145
|
+
// Simulate the field mounting, without involving the page layout and field-renderer plugins.
|
|
146
|
+
engine.emitter.emit('model:mounted', { model: form.subModels.grid.subModels.items[0] });
|
|
147
|
+
let pending: Promise<unknown> | undefined;
|
|
148
|
+
await act(async () => {
|
|
149
|
+
pending = form.applyFlow('eventSettings');
|
|
150
|
+
});
|
|
151
|
+
await waitFor(() => expect(request).toHaveBeenCalled());
|
|
152
|
+
await act(async () => {
|
|
153
|
+
resolveResponse?.();
|
|
154
|
+
await pending;
|
|
155
|
+
});
|
|
156
|
+
if (mode === 'default') {
|
|
157
|
+
// Resolving a default value must not overwrite an existing edit record.
|
|
158
|
+
expect(form.subModels.grid.subModels.items[0].props.initialValue).toBe('STAFF-001');
|
|
159
|
+
}
|
|
160
|
+
await waitFor(() => expect((screen.getByLabelText('staffname') as HTMLInputElement).value).toBe(expected));
|
|
161
|
+
expect(form.form.getFieldValue('staffname')).toBe(expected || null);
|
|
162
|
+
} finally {
|
|
163
|
+
resolveResponse?.();
|
|
164
|
+
await act(async () => {
|
|
165
|
+
view.unmount();
|
|
166
|
+
form.formValueRuntime?.dispose();
|
|
167
|
+
// Drain automatic linkage refreshes before another engine starts its variable batch.
|
|
168
|
+
await Promise.all(
|
|
169
|
+
dispatchEvent.mock.results.filter((result) => result.type === 'return').map((result) => result.value),
|
|
170
|
+
);
|
|
171
|
+
});
|
|
172
|
+
dispatchEvent.mockRestore();
|
|
173
|
+
engine.disposeScheduler();
|
|
174
|
+
}
|
|
175
|
+
});
|
|
@@ -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);
|
|
@@ -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 });
|
|
@@ -745,7 +751,10 @@ async function originalHandler(ctx, params) {
|
|
|
745
751
|
|
|
746
752
|
const targetInterface = targetLabelField.getInterfaceOptions();
|
|
747
753
|
|
|
748
|
-
const operator =
|
|
754
|
+
const operator =
|
|
755
|
+
targetLabelField.type === 'string' || targetLabelField.dataType === 'string'
|
|
756
|
+
? '$includes'
|
|
757
|
+
: targetInterface?.filterable?.operators?.[0]?.value || '$includes';
|
|
749
758
|
|
|
750
759
|
const searchText = ctx.inputArgs.searchText?.trim();
|
|
751
760
|
const resource = ctx.model.resource;
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
createCurrentRecordMetaFactory,
|
|
20
20
|
useFlowModel,
|
|
21
21
|
} from '@nocobase/flow-engine';
|
|
22
|
-
import { Table, Button, Space } from 'antd';
|
|
22
|
+
import { Table, Button, Space, theme } from 'antd';
|
|
23
23
|
import { uid } from '@formily/shared';
|
|
24
24
|
import classNames from 'classnames';
|
|
25
25
|
import { DragEndEvent } from '@dnd-kit/core';
|
|
@@ -190,6 +190,7 @@ const AddFieldColumn = ({ model }) => {
|
|
|
190
190
|
};
|
|
191
191
|
|
|
192
192
|
const DisplayTable = (props) => {
|
|
193
|
+
const { token } = theme.useToken();
|
|
193
194
|
const {
|
|
194
195
|
pageSize,
|
|
195
196
|
value,
|
|
@@ -269,7 +270,7 @@ const DisplayTable = (props) => {
|
|
|
269
270
|
},
|
|
270
271
|
},
|
|
271
272
|
...baseColumns,
|
|
272
|
-
{ key: 'empty' },
|
|
273
|
+
{ key: 'empty', render: () => <div style={{ minHeight: token.controlHeight }} /> },
|
|
273
274
|
].filter(Boolean),
|
|
274
275
|
) as any[];
|
|
275
276
|
|
|
@@ -283,7 +284,7 @@ const DisplayTable = (props) => {
|
|
|
283
284
|
}
|
|
284
285
|
|
|
285
286
|
return cols;
|
|
286
|
-
}, [baseColumns, enableIndexColumn, currentPage, currentPageSize, isConfigMode, model]);
|
|
287
|
+
}, [baseColumns, enableIndexColumn, currentPage, currentPageSize, isConfigMode, model, token.controlHeight]);
|
|
287
288
|
return (
|
|
288
289
|
<Table
|
|
289
290
|
tableLayout="fixed"
|
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
import {
|
|
36
36
|
buildOpenerUids,
|
|
37
37
|
LabelByField,
|
|
38
|
+
normalizeAssociationFieldNames,
|
|
38
39
|
type AssociationFieldNames,
|
|
39
40
|
useAssociationValueHydration,
|
|
40
41
|
} from './recordSelectShared';
|
|
@@ -331,6 +332,7 @@ function RecordPickerField(props) {
|
|
|
331
332
|
const { fieldNames, onClick, disabled } = props;
|
|
332
333
|
const ctx = useFlowContext();
|
|
333
334
|
const allowMultiple = canRecordPickerSelectMultiple(ctx.collectionField, props.allowMultiple);
|
|
335
|
+
const normalizedFieldNames = normalizeAssociationFieldNames(fieldNames, ctx.collectionField?.targetCollection);
|
|
334
336
|
useEffect(() => {
|
|
335
337
|
ctx.model.selectedRows.value = props.value;
|
|
336
338
|
}, [ctx.model.selectedRows, props.value]);
|
|
@@ -338,22 +340,23 @@ function RecordPickerField(props) {
|
|
|
338
340
|
model: ctx.model,
|
|
339
341
|
value: props.value,
|
|
340
342
|
isMultiple: allowMultiple,
|
|
341
|
-
fieldNames,
|
|
343
|
+
fieldNames: normalizedFieldNames,
|
|
342
344
|
onChange: props.onChange,
|
|
343
345
|
});
|
|
344
346
|
|
|
345
347
|
return (
|
|
346
348
|
<Select
|
|
347
349
|
{...props}
|
|
350
|
+
fieldNames={normalizedFieldNames}
|
|
348
351
|
open={false}
|
|
349
352
|
onClick={(e) => {
|
|
350
353
|
if (!disabled) {
|
|
351
354
|
onClick(e);
|
|
352
355
|
}
|
|
353
356
|
}}
|
|
354
|
-
value={normalizeRecordPickerValue(props.value,
|
|
357
|
+
value={normalizeRecordPickerValue(props.value, normalizedFieldNames, allowMultiple)}
|
|
355
358
|
labelRender={(item) => {
|
|
356
|
-
return <LabelByField option={item} fieldNames={
|
|
359
|
+
return <LabelByField option={item} fieldNames={normalizedFieldNames} />;
|
|
357
360
|
}}
|
|
358
361
|
labelInValue
|
|
359
362
|
mode={allowMultiple ? 'multiple' : undefined}
|
|
@@ -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();
|
|
@@ -65,6 +66,11 @@ function RemoteModelRenderer({ options }) {
|
|
|
65
66
|
|
|
66
67
|
export function CreateContent({ model, toOne = false }) {
|
|
67
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
|
+
});
|
|
68
74
|
const { Header, type } = ctx.view;
|
|
69
75
|
model._closeView = ctx.view.close;
|
|
70
76
|
return (
|
|
@@ -308,6 +314,7 @@ const LazySelect = (props: Readonly<LazySelectProps>) => {
|
|
|
308
314
|
}
|
|
309
315
|
setKeepOpenRuntime({ preventCloseOnSelect: false, open });
|
|
310
316
|
}
|
|
317
|
+
syncQuickEditPreventClose(model, open);
|
|
311
318
|
others.onDropdownVisibleChange?.(open);
|
|
312
319
|
}}
|
|
313
320
|
optionRender={({ data }) => {
|
|
@@ -583,7 +590,10 @@ async function originalHandler(ctx, params) {
|
|
|
583
590
|
|
|
584
591
|
const targetInterface = targetLabelField.getInterfaceOptions();
|
|
585
592
|
|
|
586
|
-
const operator =
|
|
593
|
+
const operator =
|
|
594
|
+
targetLabelField.type === 'string' || targetLabelField.dataType === 'string'
|
|
595
|
+
? '$includes'
|
|
596
|
+
: targetInterface?.filterable?.operators?.[0]?.value || '$includes';
|
|
587
597
|
|
|
588
598
|
const searchText = ctx.inputArgs.searchText?.trim();
|
|
589
599
|
|
|
@@ -874,6 +884,8 @@ RecordSelectFieldModel.registerFlow({
|
|
|
874
884
|
dataSourceKey: ctx.collection.dataSourceKey,
|
|
875
885
|
collectionName: ctx.collectionField?.target,
|
|
876
886
|
...(associationName && sourceId != null ? { associationName, sourceId } : {}),
|
|
887
|
+
// 上级记录暂无主键(如审批配置态表单)时不能传 associationName,否则弹窗内新建的区块会绑定到关联资源;改传 sourceAssociationName,仅用于展示“当前弹窗上级记录”变量
|
|
888
|
+
...(associationName && sourceId == null ? { sourceAssociationName: associationName } : {}),
|
|
877
889
|
collectionField: ctx.collectionField,
|
|
878
890
|
openerUids,
|
|
879
891
|
onChange: (e) => {
|