@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.
Files changed (91) hide show
  1. package/es/RouteRepository.d.ts +6 -0
  2. package/es/components/form/ScanInput/useCodeScanner.d.ts +12 -2
  3. package/es/components/form/ScanInput/zxingWasmDecoder.d.ts +9 -0
  4. package/es/flow/admin-shell/admin-layout/MobileTopbarActionsContext.d.ts +10 -0
  5. package/es/flow/components/FieldAssignValueInput.d.ts +2 -0
  6. package/es/flow/components/field-value-variable/FieldValueVariableInput.d.ts +1 -0
  7. package/es/flow/components/filter/VariableFilterItem.d.ts +10 -1
  8. package/es/flow/internal/utils/quickEditDataScope.d.ts +48 -0
  9. package/es/flow/internal/utils/quickEditViewContainer.d.ts +27 -0
  10. package/es/flow/models/actions/LinkActionUtils.d.ts +1 -0
  11. package/es/flow/models/blocks/assign-form/assignFormValue.d.ts +29 -0
  12. package/es/flow/models/blocks/form/QuickEditFormModel.d.ts +1 -0
  13. package/es/index.mjs +100 -100
  14. package/lib/index.js +102 -102
  15. package/package.json +9 -8
  16. package/src/BaseApplication.tsx +2 -0
  17. package/src/RouteRepository.ts +14 -0
  18. package/src/__tests__/RouteRepository.test.ts +51 -0
  19. package/src/__tests__/app.test.tsx +13 -0
  20. package/src/collection-manager/__tests__/field-configure.test.ts +52 -0
  21. package/src/collection-manager/field-configure.ts +2 -2
  22. package/src/components/form/ScanInput/CodeScanner.tsx +3 -1
  23. package/src/components/form/ScanInput/__tests__/CodeScanner.test.tsx +7 -1
  24. package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +182 -8
  25. package/src/components/form/ScanInput/__tests__/zxingWasmDecoder.test.ts +78 -0
  26. package/src/components/form/ScanInput/useCodeScanner.ts +135 -20
  27. package/src/components/form/ScanInput/zxingWasmDecoder.ts +64 -0
  28. package/src/flow/actions/__tests__/validation.test.ts +48 -0
  29. package/src/flow/admin-shell/admin-layout/MobileTopbarActionsContext.ts +13 -0
  30. package/src/flow/admin-shell/admin-layout/TopbarActionsBar.tsx +13 -2
  31. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.mobile.test.tsx +205 -0
  32. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +42 -23
  33. package/src/flow/components/FieldAssignValueInput.tsx +4 -0
  34. package/src/flow/components/field-value-variable/FieldValueVariableInput.tsx +21 -12
  35. package/src/flow/components/field-value-variable/__tests__/FieldValueVariableInput.test.tsx +9 -0
  36. package/src/flow/components/filter/VariableFilterItem.tsx +14 -4
  37. package/src/flow/components/filter/__tests__/VariableFilterItem.rightMetaTree.test.tsx +158 -0
  38. package/src/flow/internal/components/Markdown/DisplayMarkdown.tsx +14 -9
  39. package/src/flow/internal/components/Markdown/__tests__/DisplayMarkdown.test.tsx +46 -0
  40. package/src/flow/internal/utils/__tests__/quickEditDataScope.test.ts +156 -0
  41. package/src/flow/internal/utils/__tests__/quickEditViewContainer.test.ts +52 -0
  42. package/src/flow/internal/utils/quickEditDataScope.tsx +177 -0
  43. package/src/flow/internal/utils/quickEditViewContainer.ts +46 -0
  44. package/src/flow/models/actions/JSItemActionModel.tsx +19 -3
  45. package/src/flow/models/actions/LinkActionModel.tsx +11 -9
  46. package/src/flow/models/actions/LinkActionUtils.ts +4 -2
  47. package/src/flow/models/actions/__tests__/JSItemActionModel.test.tsx +47 -1
  48. package/src/flow/models/actions/__tests__/LinkActionModel.test.ts +14 -0
  49. package/src/flow/models/base/GridModel.tsx +0 -1
  50. package/src/flow/models/blocks/assign-form/AssignFormGridModel.tsx +22 -1
  51. package/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx +40 -12
  52. package/src/flow/models/blocks/assign-form/__tests__/assignFieldValuesFlow.editor.test.tsx +140 -0
  53. package/src/flow/models/blocks/assign-form/__tests__/assignFieldValuesFlow.multipleSelect.test.tsx +216 -0
  54. package/src/flow/models/blocks/assign-form/__tests__/assignFormValue.test.ts +41 -0
  55. package/src/flow/models/blocks/assign-form/assignFormValue.ts +64 -0
  56. package/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.toggleFormFieldsCollapse.test.ts +29 -0
  57. package/src/flow/models/blocks/filter-form/fields/FieldComponentProps.tsx +1 -1
  58. package/src/flow/models/blocks/filter-form/fields/__tests__/FieldComponentProps.options.test.tsx +61 -0
  59. package/src/flow/models/blocks/form/FormBlockModel.tsx +26 -5
  60. package/src/flow/models/blocks/form/QuickEditFormModel.tsx +45 -11
  61. package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +145 -0
  62. package/src/flow/models/blocks/form/__tests__/QuickEditFormModel.quickEdit.test.ts +120 -0
  63. package/src/flow/models/blocks/form/__tests__/popupLinkage.test.tsx +175 -0
  64. package/src/flow/models/blocks/js-block/JSBlock.tsx +5 -1
  65. package/src/flow/models/blocks/js-block/__tests__/JSBlockModel.test.tsx +23 -1
  66. package/src/flow/models/blocks/table/TableColumnModel.tsx +31 -0
  67. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +64 -0
  68. package/src/flow/models/fields/AssociationFieldModel/CascadeSelectFieldModel.tsx +11 -2
  69. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +4 -3
  70. package/src/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.tsx +6 -3
  71. package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +13 -1
  72. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +47 -17
  73. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +6 -1
  74. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableColumnModel.largeField.test.tsx +178 -0
  75. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableField.disabled.test.tsx +90 -0
  76. package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +26 -2
  77. package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordSelectFieldModel.quickCreatePopup.test.tsx +183 -0
  78. package/src/flow/models/fields/AssociationFieldModel/__tests__/associationSelect.search.test.ts +113 -0
  79. package/src/flow/models/fields/DisplayAssociationField/DisplaySubTableFieldModel.tsx +42 -7
  80. package/src/flow/models/fields/DisplayAssociationField/__tests__/DisplaySubTableFieldModel.test.tsx +351 -0
  81. package/src/flow/models/fields/DisplayHtmlFieldModel.tsx +2 -1
  82. package/src/flow/models/fields/InputFieldModel.tsx +1 -1
  83. package/src/flow/models/fields/RichTextFieldModel/__tests__/RichTextFieldModel.test.tsx +74 -0
  84. package/src/flow/models/fields/RichTextFieldModel/index.tsx +27 -6
  85. package/src/flow/models/fields/SelectFieldModel.tsx +5 -0
  86. package/src/flow/models/fields/__tests__/InputFieldModel.test.tsx +16 -0
  87. package/src/flow/models/fields/__tests__/SelectFieldModel.test.tsx +29 -1
  88. package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +1 -0
  89. package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +20 -0
  90. package/src/flow/models/topbar/TopbarActionModel.tsx +5 -5
  91. 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: 0,
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({ onDropdownVisibleChange: fn });
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 = targetInterface?.filterable?.operators?.[0]?.value || '$includes';
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, fieldNames, allowMultiple)}
357
+ value={normalizeRecordPickerValue(props.value, normalizedFieldNames, allowMultiple)}
355
358
  labelRender={(item) => {
356
- return <LabelByField option={item} fieldNames={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 = targetInterface?.filterable?.operators?.[0]?.value || '$includes';
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) => {