@nocobase/client-v2 2.2.0-alpha.6 → 2.2.0-alpha.7

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 (35) hide show
  1. package/es/BaseApplication.d.ts +1 -0
  2. package/es/collection-field-interface/CollectionFieldInterface.d.ts +1 -0
  3. package/es/collection-field-interface/CollectionFieldInterfaceManager.d.ts +1 -0
  4. package/es/flow/components/FieldAssignExactDatePicker.d.ts +1 -0
  5. package/es/flow/components/FieldAssignValueInput.d.ts +1 -0
  6. package/es/flow/components/RunJSValueEditor.d.ts +1 -0
  7. package/es/flow/models/blocks/form/QuickEditFormModel.d.ts +17 -2
  8. package/es/index.mjs +86 -86
  9. package/lib/index.js +95 -95
  10. package/package.json +8 -7
  11. package/src/BaseApplication.tsx +9 -5
  12. package/src/__tests__/app.test.tsx +26 -0
  13. package/src/collection-field-interface/CollectionFieldInterface.ts +1 -0
  14. package/src/collection-field-interface/CollectionFieldInterfaceManager.ts +1 -0
  15. package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +145 -2
  16. package/src/components/form/ScanInput/useCodeScanner.ts +154 -2
  17. package/src/flow/FlowPage.tsx +9 -1
  18. package/src/flow/__tests__/FlowPage.test.tsx +50 -3
  19. package/src/flow/__tests__/FlowRoute.test.tsx +2 -2
  20. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +0 -1
  21. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +253 -6
  22. package/src/flow/components/FieldAssignExactDatePicker.tsx +25 -11
  23. package/src/flow/components/FieldAssignValueInput.tsx +60 -15
  24. package/src/flow/components/FlowRoute.tsx +7 -3
  25. package/src/flow/components/RunJSValueEditor.tsx +9 -1
  26. package/src/flow/components/__tests__/FieldAssignValueInput.context.test.tsx +134 -0
  27. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +1 -0
  28. package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +27 -12
  29. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +36 -0
  30. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +34 -0
  31. package/src/flow/models/blocks/form/QuickEditFormModel.tsx +189 -36
  32. package/src/flow/models/blocks/form/__tests__/QuickEditFormModel.quickEdit.test.ts +350 -2
  33. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +1 -0
  34. package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +20 -10
  35. package/src/flow/models/fields/mobile-components/MobileSelect.tsx +24 -12
@@ -0,0 +1,134 @@
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, waitFor } from '@nocobase/test/client';
13
+ import { FieldAssignValueInput } from '../FieldAssignValueInput';
14
+
15
+ const { mockUseFlowContext, mockGetDefaultBindingByField } = vi.hoisted(() => ({
16
+ mockUseFlowContext: vi.fn(),
17
+ mockGetDefaultBindingByField: vi.fn(),
18
+ }));
19
+
20
+ vi.mock('@nocobase/flow-engine', async () => {
21
+ const actual = await vi.importActual<typeof import('@nocobase/flow-engine')>('@nocobase/flow-engine');
22
+ class MockEditableItemModel extends actual.EditableItemModel {}
23
+ MockEditableItemModel.getDefaultBindingByField = mockGetDefaultBindingByField;
24
+
25
+ return {
26
+ ...actual,
27
+ useFlowContext: () => mockUseFlowContext(),
28
+ VariableInput: () => <div data-testid="variable-input" />,
29
+ FlowModelRenderer: () => <div data-testid="flow-model-renderer" />,
30
+ EditableItemModel: MockEditableItemModel,
31
+ };
32
+ });
33
+
34
+ describe('FieldAssignValueInput context', () => {
35
+ it('delegates temporary field model context to the source form context', async () => {
36
+ type MockContext = {
37
+ dataSourceManager: { getDataSource: ReturnType<typeof vi.fn> };
38
+ t: (key: string) => string;
39
+ engine?: { createModel: ReturnType<typeof vi.fn> };
40
+ collection?: MockCollection;
41
+ blockModel?: MockFormModel;
42
+ };
43
+ type MockFieldModel = {
44
+ props: Record<string, unknown>;
45
+ setProps: ReturnType<typeof vi.fn>;
46
+ dispatchEvent: ReturnType<typeof vi.fn>;
47
+ remove: ReturnType<typeof vi.fn>;
48
+ };
49
+ type MockCollectionField = {
50
+ name: string;
51
+ interface: string;
52
+ uiSchema: { enum: Array<{ label: string; value: string }> };
53
+ isAssociationField: () => boolean;
54
+ getComponentProps: () => Record<string, unknown>;
55
+ };
56
+ type MockCollection = {
57
+ dataSourceKey: string;
58
+ name: string;
59
+ getField: (name: string) => MockCollectionField | null;
60
+ getFields: () => MockCollectionField[];
61
+ };
62
+ type MockFormModel = {
63
+ context: MockContext;
64
+ collection: MockCollection;
65
+ subModels: Record<string, unknown>;
66
+ };
67
+
68
+ const sourceContext: MockContext = {
69
+ dataSourceManager: {
70
+ getDataSource: vi.fn(() => ({})),
71
+ },
72
+ t: (key: string) => key,
73
+ };
74
+ const fieldModel: MockFieldModel = {
75
+ props: {},
76
+ setProps: vi.fn((props: Record<string, unknown>) => {
77
+ fieldModel.props = { ...fieldModel.props, ...props };
78
+ }),
79
+ dispatchEvent: vi.fn(),
80
+ remove: vi.fn(),
81
+ };
82
+ const tempRoot = {
83
+ context: {
84
+ defineProperty: vi.fn(),
85
+ },
86
+ subModels: {
87
+ fields: [fieldModel],
88
+ },
89
+ setProps: vi.fn(),
90
+ remove: vi.fn(),
91
+ };
92
+ const engine = {
93
+ createModel: vi.fn(() => tempRoot),
94
+ };
95
+ sourceContext.engine = engine;
96
+
97
+ const collectionField: MockCollectionField = {
98
+ name: 'status',
99
+ interface: 'select',
100
+ uiSchema: {
101
+ enum: [{ label: 'Open', value: 'open' }],
102
+ },
103
+ isAssociationField: () => false,
104
+ getComponentProps: () => ({}),
105
+ };
106
+ const collection: MockCollection = {
107
+ dataSourceKey: 'main',
108
+ name: 'tasks',
109
+ getField: (name: string) => (name === 'status' ? collectionField : null),
110
+ getFields: () => [collectionField],
111
+ };
112
+ const formModel: MockFormModel = {
113
+ context: sourceContext,
114
+ collection,
115
+ subModels: {},
116
+ };
117
+ sourceContext.collection = collection;
118
+ sourceContext.blockModel = formModel;
119
+
120
+ mockGetDefaultBindingByField.mockReturnValue({ modelName: 'SelectFieldModel' });
121
+ mockUseFlowContext.mockReturnValue({
122
+ model: formModel,
123
+ t: (key: string) => key,
124
+ getPropertyMetaTree: vi.fn(async () => []),
125
+ });
126
+
127
+ render(<FieldAssignValueInput targetPath="status" value="" onChange={vi.fn()} />);
128
+
129
+ await waitFor(() => {
130
+ expect(engine.createModel).toHaveBeenCalled();
131
+ });
132
+ expect(engine.createModel).toHaveBeenCalledWith(expect.any(Object), { delegate: sourceContext });
133
+ });
134
+ });
@@ -431,6 +431,7 @@ export class FilterFormBlockModel extends FilterBlockModel<{
431
431
 
432
432
  private canApplyFormDefaultValue(name: string, current: any, force?: boolean) {
433
433
  if (force) return true;
434
+ if (this.userEditedFieldNames.has(name)) return false;
434
435
  if (isEmptyValue(current)) return true;
435
436
  if (!this.lastDefaultValueByFieldName.has(name)) return false;
436
437
  return isEqual(current, this.lastDefaultValueByFieldName.get(name));
@@ -62,9 +62,25 @@ function isRecord(value: unknown): value is Record<string, unknown> {
62
62
  return !!value && typeof value === 'object' && !Array.isArray(value);
63
63
  }
64
64
 
65
+ interface CollectionFieldWithAssociationMarker {
66
+ isAssociationField?: () => boolean;
67
+ target?: unknown;
68
+ }
69
+
70
+ function hasAssociationMarker(value: unknown): value is CollectionFieldWithAssociationMarker {
71
+ return isRecord(value) && (typeof value.isAssociationField === 'function' || 'target' in value);
72
+ }
73
+
74
+ const isAssociationCollectionField = (collectionField: unknown) => {
75
+ if (!hasAssociationMarker(collectionField)) {
76
+ return false;
77
+ }
78
+ return collectionField.isAssociationField ? collectionField.isAssociationField() : Boolean(collectionField.target);
79
+ };
80
+
65
81
  const normalizeAssociationDefaultFilterValue = (value: any, fieldModel: any) => {
66
82
  const collectionField = fieldModel?.context?.collectionField;
67
- if (!collectionField?.isAssociationField?.()) {
83
+ if (!isAssociationCollectionField(collectionField)) {
68
84
  return value;
69
85
  }
70
86
 
@@ -171,8 +187,7 @@ const buildFilterFormFieldItem = ({
171
187
  if (!binding) {
172
188
  return;
173
189
  }
174
- const isAssociation =
175
- typeof field?.isAssociationField === 'function' ? field.isAssociationField() : Boolean(field?.target);
190
+ const isAssociation = isAssociationCollectionField(field);
176
191
  const fieldModel =
177
192
  isAssociation && ctxWithFlags.engine?.getModelClass?.('FilterFormRecordSelectFieldModel')
178
193
  ? 'FilterFormRecordSelectFieldModel'
@@ -507,10 +522,7 @@ export class FilterFormItemModel extends FilterableItemModel<{
507
522
  const formValue = this.context.form?.getFieldValue(this.props.name);
508
523
  const modelValue = fieldModel.getFilterValue ? fieldModel.getFilterValue() : formValue;
509
524
  const collectionField = (fieldModel as any)?.context?.collectionField;
510
- const isAssociationField =
511
- typeof collectionField?.isAssociationField === 'function'
512
- ? collectionField.isAssociationField()
513
- : !!collectionField?.target;
525
+ const isAssociationField = isAssociationCollectionField(collectionField);
514
526
  const shouldUseFormValue =
515
527
  isAssociationField &&
516
528
  !this.mounted &&
@@ -547,11 +559,7 @@ export class FilterFormItemModel extends FilterableItemModel<{
547
559
  return value;
548
560
  }
549
561
  const collectionField = (fieldModel as any)?.context?.collectionField;
550
- const isAssociation =
551
- typeof collectionField?.isAssociationField === 'function'
552
- ? collectionField.isAssociationField()
553
- : !!collectionField?.target;
554
- if (!isAssociation) {
562
+ if (!isAssociationCollectionField(collectionField)) {
555
563
  return value;
556
564
  }
557
565
  const normalizedFieldNames = normalizeAssociationFieldNames(
@@ -798,6 +806,13 @@ FilterFormItemModel.registerFlow({
798
806
  },
799
807
  defaultOperator: {
800
808
  use: 'defaultOperator',
809
+ hideInSettings(ctx) {
810
+ const collectionField =
811
+ ctx.collectionField ||
812
+ ctx.model?.context?.collectionField ||
813
+ ctx.model?.subModels?.field?.context?.collectionField;
814
+ return isAssociationCollectionField(collectionField);
815
+ },
801
816
  },
802
817
  operatorComponentProps: {
803
818
  use: 'operatorComponentProps',
@@ -33,6 +33,42 @@ class DummyCollectionBlockModel extends CollectionBlockModel {
33
33
  }
34
34
 
35
35
  describe('FilterFormItemModel defineChildren association fields', () => {
36
+ it('hides default operator setting for association filter fields', () => {
37
+ const engine = new FlowEngine();
38
+ engine.registerModels({
39
+ FilterFormItemModel,
40
+ });
41
+
42
+ const filterItem = engine.createModel<FilterFormItemModel>({
43
+ uid: 'association-filter-item-settings',
44
+ use: 'FilterFormItemModel',
45
+ });
46
+
47
+ const defaultOperatorStep = filterItem.getFlow('filterFormItemSettings')?.steps?.defaultOperator as {
48
+ hideInSettings?: (ctx: {
49
+ collectionField?: unknown;
50
+ model?: {
51
+ subModels?: {
52
+ field?: {
53
+ context?: {
54
+ collectionField?: unknown;
55
+ };
56
+ };
57
+ };
58
+ };
59
+ }) => boolean;
60
+ };
61
+ expect(defaultOperatorStep?.hideInSettings?.({ collectionField: { isAssociationField: () => true } })).toBe(true);
62
+ expect(
63
+ defaultOperatorStep?.hideInSettings?.({
64
+ model: { subModels: { field: { context: { collectionField: { target: 'departments' } } } } },
65
+ }),
66
+ ).toBe(true);
67
+ expect(defaultOperatorStep?.hideInSettings?.({ collectionField: { interface: 'input', type: 'string' } })).toBe(
68
+ false,
69
+ );
70
+ });
71
+
36
72
  it('groups association target fields and supports recursive paths', async () => {
37
73
  const engine = new FlowEngine();
38
74
  engine.registerModels({
@@ -301,6 +301,40 @@ describe('filter-form defaultValues wiring', () => {
301
301
  expect(values.username_user).toBe('Manual');
302
302
  });
303
303
 
304
+ it('does not reapply a filter form default value after user clears the field', async () => {
305
+ const { model, values } = createFilterFormDefaultValuesModel([
306
+ {
307
+ key: 'username-default',
308
+ enable: true,
309
+ targetPath: 'username',
310
+ mode: 'default',
311
+ value: 'admin',
312
+ },
313
+ ]);
314
+
315
+ await FilterFormBlockModel.prototype.applyFormDefaultValues.call(model as any);
316
+ expect(values.username_user).toBe('admin');
317
+
318
+ values.username_user = undefined;
319
+ (model as any).handleFilterFormValuesChange({ username_user: undefined }, { username_user: undefined });
320
+
321
+ await waitFor(() => {
322
+ expect(model.dispatchEvent).toHaveBeenCalledWith(
323
+ 'formValuesChange',
324
+ {
325
+ changedValues: {
326
+ username_user: undefined,
327
+ },
328
+ allValues: {
329
+ username_user: undefined,
330
+ },
331
+ },
332
+ { debounce: true },
333
+ );
334
+ });
335
+ expect(values.username_user).toBeUndefined();
336
+ });
337
+
304
338
  it('applies fixed values even when the target filter field already has a value', async () => {
305
339
  const { model, values } = createFilterFormDefaultValuesModel(
306
340
  [
@@ -22,7 +22,6 @@ import {
22
22
  createRecordResolveOnServerWithLocal,
23
23
  } from '@nocobase/flow-engine';
24
24
  import { Button, Form, Skeleton, Space } from 'antd';
25
- import _ from 'lodash';
26
25
  import React from 'react';
27
26
  import { FieldModel } from '../../base/FieldModel';
28
27
  import { FormComponent } from './FormBlockModel';
@@ -31,6 +30,37 @@ import { FormItemModel } from './FormItemModel';
31
30
  export const QUICK_EDIT_POPOVER_MAX_HEIGHT = 'calc(100vh - 96px)';
32
31
  export const QUICK_EDIT_FORM_MAX_HEIGHT = 'calc(100vh - 160px)';
33
32
  export const QUICK_EDIT_MARKDOWN_HEIGHT = 'min(480px, calc(100vh - 320px))';
33
+ const QUICK_EDIT_MOBILE_DRAWER_HEIGHT = '50vh';
34
+ const QUICK_EDIT_MOBILE_FORM_MAX_HEIGHT = 'calc(50vh - var(--nb-mobile-page-header-height, 40px) - 132px)';
35
+ const QUICK_EDIT_MOBILE_CONTENT_PADDING = '8px var(--nb-mobile-page-tabs-content-padding, 12px) 0';
36
+ const QUICK_EDIT_MOBILE_ACTIONS_PADDING =
37
+ '8px var(--nb-mobile-page-tabs-content-padding, 12px) calc(80px + env(safe-area-inset-bottom, 0px))';
38
+ const QUICK_EDIT_MOBILE_MEDIA_QUERY = '(max-width: 768px)';
39
+
40
+ type QuickEditViewBeforeClosePayload = {
41
+ result?: unknown;
42
+ force?: boolean;
43
+ [key: string]: unknown;
44
+ };
45
+
46
+ type QuickEditViewBeforeCloseHandler = (
47
+ payload: QuickEditViewBeforeClosePayload,
48
+ ) => Promise<boolean | void> | boolean | void;
49
+
50
+ type QuickEditViewUpdateConfig = {
51
+ preventClose?: boolean;
52
+ [key: string]: unknown;
53
+ };
54
+
55
+ type QuickEditViewContainer = {
56
+ close: (result?: unknown, force?: boolean) => Promise<boolean | void> | boolean | void;
57
+ update?: (newConfig: QuickEditViewUpdateConfig) => unknown;
58
+ beforeClose?: QuickEditViewBeforeCloseHandler;
59
+ };
60
+
61
+ type QuickEditViewContext = {
62
+ defineProperty?: (key: string, options: { value: unknown }) => void;
63
+ };
34
64
 
35
65
  export function getQuickEditFieldProps(collectionField: CollectionField, fieldProps?: Record<string, any>) {
36
66
  const nextProps = { ...collectionField.getComponentProps(), ...(fieldProps || {}) };
@@ -40,15 +70,92 @@ export function getQuickEditFieldProps(collectionField: CollectionField, fieldPr
40
70
  return nextProps;
41
71
  }
42
72
 
73
+ function getQuickEditMobileLayoutState(flowEngine: FlowEngine, sourceFieldModel?: FlowModel) {
74
+ const sourceMobileLayout = sourceFieldModel?.context?.isMobileLayout;
75
+ if (typeof sourceMobileLayout === 'boolean') {
76
+ return { isMobileLayout: sourceMobileLayout, inheritsMobileContext: sourceMobileLayout };
77
+ }
78
+
79
+ const engineMobileLayout = flowEngine.context?.isMobileLayout;
80
+ if (typeof engineMobileLayout === 'boolean') {
81
+ return { isMobileLayout: engineMobileLayout, inheritsMobileContext: engineMobileLayout };
82
+ }
83
+
84
+ if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {
85
+ return { isMobileLayout: false, inheritsMobileContext: false };
86
+ }
87
+
88
+ return {
89
+ isMobileLayout: window.matchMedia(QUICK_EDIT_MOBILE_MEDIA_QUERY).matches,
90
+ inheritsMobileContext: false,
91
+ };
92
+ }
93
+
94
+ function getQuickEditTitle(
95
+ flowEngine: FlowEngine,
96
+ dataSourceKey: string,
97
+ collectionName: string,
98
+ fieldPath: string,
99
+ sourceFieldModel?: FlowModel,
100
+ fieldProps?: Record<string, unknown>,
101
+ ): React.ReactNode {
102
+ const sourceColumnTitle = sourceFieldModel?.parent?.props?.title;
103
+ if (sourceColumnTitle) {
104
+ return sourceColumnTitle;
105
+ }
106
+
107
+ const fieldPropsTitle = fieldProps?.title;
108
+ if (fieldPropsTitle) {
109
+ return fieldPropsTitle as React.ReactNode;
110
+ }
111
+
112
+ const collectionField = flowEngine.context.dataSourceManager.getCollectionField(
113
+ `${dataSourceKey}.${collectionName}.${fieldPath}`,
114
+ ) as CollectionField | undefined;
115
+ return collectionField?.title || fieldPath;
116
+ }
117
+
118
+ function createQuickEditViewContainer(view: QuickEditViewContainer): QuickEditViewContainer {
119
+ let preventClose = false;
120
+ let nextBeforeClose = view.beforeClose;
121
+ const beforeClose: QuickEditViewBeforeCloseHandler = async (payload) => {
122
+ if (preventClose && !payload?.force) {
123
+ return false;
124
+ }
125
+
126
+ const result = await nextBeforeClose?.(payload);
127
+ return result !== false;
128
+ };
129
+ view.beforeClose = beforeClose;
130
+
131
+ return {
132
+ ...view,
133
+ close(result, force) {
134
+ return view.close(result, force);
135
+ },
136
+ update(newConfig) {
137
+ if (Object.prototype.hasOwnProperty.call(newConfig, 'preventClose')) {
138
+ preventClose = !!newConfig.preventClose;
139
+ }
140
+ return view.update?.(newConfig);
141
+ },
142
+ get beforeClose() {
143
+ return view.beforeClose;
144
+ },
145
+ set beforeClose(value) {
146
+ nextBeforeClose = value;
147
+ view.beforeClose = beforeClose;
148
+ },
149
+ };
150
+ }
151
+
43
152
  export class QuickEditFormModel extends FlowModel {
44
153
  fieldPath: string;
45
154
 
46
155
  declare resource: SingleRecordResource;
47
156
  declare collection: Collection;
48
157
 
49
- now: number = Date.now();
50
-
51
- viewContainer: any;
158
+ declare viewContainer: QuickEditViewContainer;
52
159
  __onSubmitSuccess;
53
160
  _fieldProps: any;
54
161
  _onOk: any;
@@ -89,21 +196,75 @@ export class QuickEditFormModel extends FlowModel {
89
196
  onOk,
90
197
  sourceFieldModelUid,
91
198
  } = options;
92
- const model = flowEngine.createModel({
93
- use: 'QuickEditFormModel',
94
- stepParams: {
95
- quickEditFormSettings: {
96
- init: {
97
- dataSourceKey,
98
- collectionName,
99
- fieldPath,
199
+ const sourceFieldModel = sourceFieldModelUid ? flowEngine.getModel<FlowModel>(sourceFieldModelUid) : undefined;
200
+ const model = flowEngine.createModel(
201
+ {
202
+ use: 'QuickEditFormModel',
203
+ stepParams: {
204
+ quickEditFormSettings: {
205
+ init: {
206
+ dataSourceKey,
207
+ collectionName,
208
+ fieldPath,
209
+ },
100
210
  },
101
211
  },
102
212
  },
103
- }) as QuickEditFormModel;
213
+ sourceFieldModel ? { delegate: sourceFieldModel.context } : undefined,
214
+ ) as QuickEditFormModel;
104
215
 
105
- console.log('QuickEditFormModel.open2', Date.now() - model.now);
106
- model.now = Date.now();
216
+ const bodyStyles = {
217
+ maxHeight: QUICK_EDIT_POPOVER_MAX_HEIGHT,
218
+ overflowY: 'auto',
219
+ overscrollBehavior: 'contain',
220
+ };
221
+ const mobileBodyStyles = {
222
+ ...bodyStyles,
223
+ maxHeight: QUICK_EDIT_MOBILE_DRAWER_HEIGHT,
224
+ };
225
+ const content = (view: QuickEditViewContainer, viewContext?: QuickEditViewContext) => {
226
+ if (mobileLayoutState.isMobileLayout) {
227
+ viewContext?.defineProperty?.('isMobileLayout', { value: true });
228
+ }
229
+ model.viewContainer = createQuickEditViewContainer(view);
230
+ model.__onSubmitSuccess = onSuccess;
231
+ model._fieldProps = fieldProps;
232
+ model._onOk = onOk;
233
+ return (
234
+ <FlowModelRenderer
235
+ fallback={<Skeleton.Input size="small" />}
236
+ model={model}
237
+ inputArgs={{ filterByTk, record, sourceFieldModelUid }}
238
+ />
239
+ );
240
+ };
241
+ const mobileLayoutState = getQuickEditMobileLayoutState(flowEngine, sourceFieldModel);
242
+ if (mobileLayoutState.isMobileLayout) {
243
+ model.context.defineProperty('isMobileLayout', { value: true });
244
+ const viewer = sourceFieldModel?.context?.viewer || flowEngine.context.viewer;
245
+ const title = getQuickEditTitle(
246
+ flowEngine,
247
+ dataSourceKey,
248
+ collectionName,
249
+ fieldPath,
250
+ sourceFieldModel,
251
+ fieldProps,
252
+ );
253
+ await viewer.open({
254
+ type: 'drawer',
255
+ title,
256
+ closable: true,
257
+ placement: 'bottom',
258
+ inputArgs: {
259
+ isMobileLayout: true,
260
+ },
261
+ styles: {
262
+ body: mobileBodyStyles,
263
+ },
264
+ content,
265
+ });
266
+ return;
267
+ }
107
268
 
108
269
  await flowEngine.context.viewer.open({
109
270
  type: 'popover',
@@ -112,25 +273,10 @@ export class QuickEditFormModel extends FlowModel {
112
273
  styles: {
113
274
  body: {
114
275
  width: 420,
115
- maxHeight: QUICK_EDIT_POPOVER_MAX_HEIGHT,
116
- overflowY: 'auto',
117
- overscrollBehavior: 'contain',
276
+ ...bodyStyles,
118
277
  },
119
278
  },
120
- content: (popover) => {
121
- model.viewContainer = popover;
122
- model.__onSubmitSuccess = onSuccess;
123
- model._fieldProps = fieldProps;
124
- model._onOk = onOk;
125
- console.log('QuickEditFormModel.open3', Date.now() - model.now);
126
- return (
127
- <FlowModelRenderer
128
- fallback={<Skeleton.Input size="small" />}
129
- model={model}
130
- inputArgs={{ filterByTk, record, sourceFieldModelUid }}
131
- />
132
- );
133
- },
279
+ content,
134
280
  });
135
281
  }
136
282
 
@@ -171,15 +317,15 @@ export class QuickEditFormModel extends FlowModel {
171
317
  }
172
318
 
173
319
  render() {
174
- console.log('QuickEditFormModel.open4', Date.now() - this.now);
175
-
320
+ const isMobileLayout = this.context.isMobileLayout;
176
321
  return (
177
322
  <FormComponent model={this}>
178
323
  <div
179
324
  style={{
180
325
  minHeight: 0,
181
326
  overflowY: 'auto',
182
- maxHeight: QUICK_EDIT_FORM_MAX_HEIGHT,
327
+ maxHeight: isMobileLayout ? QUICK_EDIT_MOBILE_FORM_MAX_HEIGHT : QUICK_EDIT_FORM_MAX_HEIGHT,
328
+ padding: isMobileLayout ? QUICK_EDIT_MOBILE_CONTENT_PADDING : undefined,
183
329
  }}
184
330
  >
185
331
  {this.mapSubModels('fields', (field) => {
@@ -196,7 +342,14 @@ export class QuickEditFormModel extends FlowModel {
196
342
  );
197
343
  })}
198
344
  </div>
199
- <Space style={{ display: 'flex', justifyContent: 'flex-end', flexShrink: 0 }}>
345
+ <Space
346
+ style={{
347
+ display: 'flex',
348
+ justifyContent: 'flex-end',
349
+ flexShrink: 0,
350
+ padding: isMobileLayout ? QUICK_EDIT_MOBILE_ACTIONS_PADDING : undefined,
351
+ }}
352
+ >
200
353
  <Button
201
354
  onClick={() => {
202
355
  this.viewContainer.close();