@nocobase/client-v2 2.1.20 → 2.1.22
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/collection-field-interface/CollectionFieldInterface.d.ts +1 -0
- package/es/collection-field-interface/CollectionFieldInterfaceManager.d.ts +1 -0
- package/es/flow/models/blocks/form/QuickEditFormModel.d.ts +17 -1
- package/es/index.mjs +62 -62
- package/lib/index.js +69 -69
- package/package.json +7 -7
- package/src/collection-field-interface/CollectionFieldInterface.ts +1 -0
- package/src/collection-field-interface/CollectionFieldInterfaceManager.ts +1 -0
- package/src/flow/FlowPage.tsx +9 -1
- package/src/flow/__tests__/FlowPage.test.tsx +50 -3
- package/src/flow/components/FieldAssignValueInput.tsx +15 -11
- package/src/flow/components/__tests__/FieldAssignValueInput.context.test.tsx +134 -0
- package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +1 -0
- package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +27 -12
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +36 -0
- package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +34 -0
- package/src/flow/models/blocks/form/QuickEditFormModel.tsx +177 -19
- package/src/flow/models/blocks/form/__tests__/QuickEditFormModel.quickEdit.test.ts +320 -1
- package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +20 -10
- package/src/flow/models/fields/mobile-components/MobileSelect.tsx +24 -12
|
@@ -30,6 +30,37 @@ import { FormItemModel } from './FormItemModel';
|
|
|
30
30
|
export const QUICK_EDIT_POPOVER_MAX_HEIGHT = 'calc(100vh - 96px)';
|
|
31
31
|
export const QUICK_EDIT_FORM_MAX_HEIGHT = 'calc(100vh - 160px)';
|
|
32
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
|
+
};
|
|
33
64
|
|
|
34
65
|
export function getQuickEditFieldProps(collectionField: CollectionField, fieldProps?: Record<string, any>) {
|
|
35
66
|
const nextProps = { ...collectionField.getComponentProps(), ...(fieldProps || {}) };
|
|
@@ -39,13 +70,92 @@ export function getQuickEditFieldProps(collectionField: CollectionField, fieldPr
|
|
|
39
70
|
return nextProps;
|
|
40
71
|
}
|
|
41
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
|
+
|
|
42
152
|
export class QuickEditFormModel extends FlowModel {
|
|
43
153
|
fieldPath: string;
|
|
44
154
|
|
|
45
155
|
declare resource: SingleRecordResource;
|
|
46
156
|
declare collection: Collection;
|
|
47
157
|
|
|
48
|
-
viewContainer:
|
|
158
|
+
declare viewContainer: QuickEditViewContainer;
|
|
49
159
|
__onSubmitSuccess;
|
|
50
160
|
_fieldProps: any;
|
|
51
161
|
_onOk: any;
|
|
@@ -103,6 +213,59 @@ export class QuickEditFormModel extends FlowModel {
|
|
|
103
213
|
sourceFieldModel ? { delegate: sourceFieldModel.context } : undefined,
|
|
104
214
|
) as QuickEditFormModel;
|
|
105
215
|
|
|
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
|
+
}
|
|
268
|
+
|
|
106
269
|
await flowEngine.context.viewer.open({
|
|
107
270
|
type: 'popover',
|
|
108
271
|
target,
|
|
@@ -110,24 +273,10 @@ export class QuickEditFormModel extends FlowModel {
|
|
|
110
273
|
styles: {
|
|
111
274
|
body: {
|
|
112
275
|
width: 420,
|
|
113
|
-
|
|
114
|
-
overflowY: 'auto',
|
|
115
|
-
overscrollBehavior: 'contain',
|
|
276
|
+
...bodyStyles,
|
|
116
277
|
},
|
|
117
278
|
},
|
|
118
|
-
content
|
|
119
|
-
model.viewContainer = popover;
|
|
120
|
-
model.__onSubmitSuccess = onSuccess;
|
|
121
|
-
model._fieldProps = fieldProps;
|
|
122
|
-
model._onOk = onOk;
|
|
123
|
-
return (
|
|
124
|
-
<FlowModelRenderer
|
|
125
|
-
fallback={<Skeleton.Input size="small" />}
|
|
126
|
-
model={model}
|
|
127
|
-
inputArgs={{ filterByTk, record, sourceFieldModelUid }}
|
|
128
|
-
/>
|
|
129
|
-
);
|
|
130
|
-
},
|
|
279
|
+
content,
|
|
131
280
|
});
|
|
132
281
|
}
|
|
133
282
|
|
|
@@ -168,13 +317,15 @@ export class QuickEditFormModel extends FlowModel {
|
|
|
168
317
|
}
|
|
169
318
|
|
|
170
319
|
render() {
|
|
320
|
+
const isMobileLayout = this.context.isMobileLayout;
|
|
171
321
|
return (
|
|
172
322
|
<FormComponent model={this}>
|
|
173
323
|
<div
|
|
174
324
|
style={{
|
|
175
325
|
minHeight: 0,
|
|
176
326
|
overflowY: 'auto',
|
|
177
|
-
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,
|
|
178
329
|
}}
|
|
179
330
|
>
|
|
180
331
|
{this.mapSubModels('fields', (field) => {
|
|
@@ -191,7 +342,14 @@ export class QuickEditFormModel extends FlowModel {
|
|
|
191
342
|
);
|
|
192
343
|
})}
|
|
193
344
|
</div>
|
|
194
|
-
<Space
|
|
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
|
+
>
|
|
195
353
|
<Button
|
|
196
354
|
onClick={() => {
|
|
197
355
|
this.viewContainer.close();
|
|
@@ -7,17 +7,54 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
10
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
11
11
|
import { FlowEngine, FlowModel, SingleRecordResource } from '@nocobase/flow-engine';
|
|
12
12
|
import { QuickEditFormModel } from '../QuickEditFormModel';
|
|
13
13
|
|
|
14
14
|
describe('QuickEditFormModel - quick edit save triggers API (regression)', () => {
|
|
15
15
|
let engine: FlowEngine;
|
|
16
|
+
const originalMatchMediaDescriptor = Object.getOwnPropertyDescriptor(window, 'matchMedia');
|
|
16
17
|
|
|
17
18
|
beforeEach(() => {
|
|
18
19
|
engine = new FlowEngine();
|
|
19
20
|
});
|
|
20
21
|
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
if (originalMatchMediaDescriptor) {
|
|
24
|
+
Object.defineProperty(window, 'matchMedia', originalMatchMediaDescriptor);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
delete (window as unknown as { matchMedia?: Window['matchMedia'] }).matchMedia;
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const mockMatchMedia = (matches: boolean) => {
|
|
31
|
+
const matchMediaMock = vi.fn((query: string) => {
|
|
32
|
+
return {
|
|
33
|
+
matches,
|
|
34
|
+
media: query,
|
|
35
|
+
onchange: null,
|
|
36
|
+
addListener: vi.fn(),
|
|
37
|
+
removeListener: vi.fn(),
|
|
38
|
+
addEventListener: vi.fn(),
|
|
39
|
+
removeEventListener: vi.fn(),
|
|
40
|
+
dispatchEvent: vi.fn(),
|
|
41
|
+
} as unknown as MediaQueryList;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
Object.defineProperty(window, 'matchMedia', {
|
|
45
|
+
...(originalMatchMediaDescriptor || { configurable: true, writable: true }),
|
|
46
|
+
value: matchMediaMock,
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const addUsersCollection = () => {
|
|
51
|
+
const ds = engine.context.dataSourceManager.getDataSource('main');
|
|
52
|
+
ds.addCollection({
|
|
53
|
+
name: 'users',
|
|
54
|
+
fields: [{ name: 'name', type: 'string', interface: 'input', uiSchema: { title: 'Name' } }],
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
|
|
21
58
|
it('uses source field context when opening quick edit', async () => {
|
|
22
59
|
engine.registerModels({ QuickEditFormModel });
|
|
23
60
|
engine.context.defineProperty('pageActive', { value: { value: false } });
|
|
@@ -47,6 +84,288 @@ describe('QuickEditFormModel - quick edit save triggers API (regression)', () =>
|
|
|
47
84
|
expect(quickEditModel?.context.pageActive.value).toBe(true);
|
|
48
85
|
});
|
|
49
86
|
|
|
87
|
+
it('opens quick edit in a mobile drawer when the source field layout is mobile', async () => {
|
|
88
|
+
engine.registerModels({ QuickEditFormModel });
|
|
89
|
+
addUsersCollection();
|
|
90
|
+
const engineOpen = vi.fn(async () => undefined);
|
|
91
|
+
const sourceOpen = vi.fn(async () => undefined);
|
|
92
|
+
engine.context.defineProperty('isMobileLayout', { value: false });
|
|
93
|
+
engine.context.defineProperty('viewer', { value: { open: engineOpen } });
|
|
94
|
+
const source = engine.createModel<FlowModel>({ use: 'FlowModel', uid: 'source-field' });
|
|
95
|
+
source.context.defineProperty('isMobileLayout', { value: true });
|
|
96
|
+
source.context.defineProperty('viewer', { value: { open: sourceOpen } });
|
|
97
|
+
|
|
98
|
+
await QuickEditFormModel.open({
|
|
99
|
+
flowEngine: engine,
|
|
100
|
+
target: document.createElement('div'),
|
|
101
|
+
dataSourceKey: 'main',
|
|
102
|
+
collectionName: 'users',
|
|
103
|
+
fieldPath: 'name',
|
|
104
|
+
record: {},
|
|
105
|
+
sourceFieldModelUid: source.uid,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
expect(engineOpen).not.toHaveBeenCalled();
|
|
109
|
+
expect(sourceOpen).toHaveBeenCalledTimes(1);
|
|
110
|
+
expect(sourceOpen).toHaveBeenCalledWith(
|
|
111
|
+
expect.objectContaining({
|
|
112
|
+
type: 'drawer',
|
|
113
|
+
title: 'Name',
|
|
114
|
+
closable: true,
|
|
115
|
+
placement: 'bottom',
|
|
116
|
+
styles: {
|
|
117
|
+
body: expect.objectContaining({
|
|
118
|
+
maxHeight: '50vh',
|
|
119
|
+
}),
|
|
120
|
+
},
|
|
121
|
+
inputArgs: {
|
|
122
|
+
isMobileLayout: true,
|
|
123
|
+
},
|
|
124
|
+
}),
|
|
125
|
+
);
|
|
126
|
+
let quickEditModel: QuickEditFormModel | undefined;
|
|
127
|
+
engine.forEachModel((model) => {
|
|
128
|
+
if (model instanceof QuickEditFormModel) {
|
|
129
|
+
quickEditModel = model;
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
expect(quickEditModel?.context.isMobileLayout).toBe(true);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('uses the source table column title for the mobile drawer header', async () => {
|
|
136
|
+
engine.registerModels({ QuickEditFormModel });
|
|
137
|
+
addUsersCollection();
|
|
138
|
+
const sourceOpen = vi.fn(async () => undefined);
|
|
139
|
+
engine.context.defineProperty('viewer', { value: { open: vi.fn(async () => undefined) } });
|
|
140
|
+
const column = engine.createModel<FlowModel>({ use: 'FlowModel', uid: 'table-column' });
|
|
141
|
+
column.setProps({ title: 'Custom marital status' });
|
|
142
|
+
const source = engine.createModel<FlowModel>({ use: 'FlowModel', uid: 'source-field', parentId: column.uid });
|
|
143
|
+
source.context.defineProperty('isMobileLayout', { value: true });
|
|
144
|
+
source.context.defineProperty('viewer', { value: { open: sourceOpen } });
|
|
145
|
+
|
|
146
|
+
await QuickEditFormModel.open({
|
|
147
|
+
flowEngine: engine,
|
|
148
|
+
target: document.createElement('div'),
|
|
149
|
+
dataSourceKey: 'main',
|
|
150
|
+
collectionName: 'users',
|
|
151
|
+
fieldPath: 'name',
|
|
152
|
+
record: {},
|
|
153
|
+
sourceFieldModelUid: source.uid,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
expect(sourceOpen).toHaveBeenCalledWith(
|
|
157
|
+
expect.objectContaining({
|
|
158
|
+
type: 'drawer',
|
|
159
|
+
title: 'Custom marital status',
|
|
160
|
+
}),
|
|
161
|
+
);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('uses the mobile drawer fallback on narrow viewports', async () => {
|
|
165
|
+
engine.registerModels({ QuickEditFormModel });
|
|
166
|
+
addUsersCollection();
|
|
167
|
+
mockMatchMedia(true);
|
|
168
|
+
const viewContext = {
|
|
169
|
+
defineProperty: vi.fn(),
|
|
170
|
+
};
|
|
171
|
+
const open = vi.fn(
|
|
172
|
+
async (config: { content: (view: { close: () => void }, context: typeof viewContext) => unknown }) => {
|
|
173
|
+
config.content({ close: vi.fn() }, viewContext);
|
|
174
|
+
},
|
|
175
|
+
);
|
|
176
|
+
engine.context.defineProperty('viewer', { value: { open } });
|
|
177
|
+
|
|
178
|
+
await QuickEditFormModel.open({
|
|
179
|
+
flowEngine: engine,
|
|
180
|
+
target: document.createElement('div'),
|
|
181
|
+
dataSourceKey: 'main',
|
|
182
|
+
collectionName: 'users',
|
|
183
|
+
fieldPath: 'name',
|
|
184
|
+
record: {},
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
expect(open).toHaveBeenCalledTimes(1);
|
|
188
|
+
expect(viewContext.defineProperty).toHaveBeenCalledWith('isMobileLayout', { value: true });
|
|
189
|
+
expect(window.matchMedia).toHaveBeenCalledWith('(max-width: 768px)');
|
|
190
|
+
expect(open).toHaveBeenCalledWith(
|
|
191
|
+
expect.objectContaining({
|
|
192
|
+
type: 'drawer',
|
|
193
|
+
title: 'Name',
|
|
194
|
+
closable: true,
|
|
195
|
+
placement: 'bottom',
|
|
196
|
+
styles: {
|
|
197
|
+
body: expect.objectContaining({
|
|
198
|
+
maxHeight: '50vh',
|
|
199
|
+
}),
|
|
200
|
+
},
|
|
201
|
+
inputArgs: {
|
|
202
|
+
isMobileLayout: true,
|
|
203
|
+
},
|
|
204
|
+
}),
|
|
205
|
+
);
|
|
206
|
+
let quickEditModel: QuickEditFormModel | undefined;
|
|
207
|
+
engine.forEachModel((model) => {
|
|
208
|
+
if (model instanceof QuickEditFormModel) {
|
|
209
|
+
quickEditModel = model;
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
expect(quickEditModel?.context.isMobileLayout).toBe(true);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('uses the engine mobile layout context without relying on matchMedia', async () => {
|
|
216
|
+
engine.registerModels({ QuickEditFormModel });
|
|
217
|
+
addUsersCollection();
|
|
218
|
+
mockMatchMedia(false);
|
|
219
|
+
const open = vi.fn(async () => undefined);
|
|
220
|
+
engine.context.defineProperty('isMobileLayout', { value: true });
|
|
221
|
+
engine.context.defineProperty('viewer', { value: { open } });
|
|
222
|
+
|
|
223
|
+
await QuickEditFormModel.open({
|
|
224
|
+
flowEngine: engine,
|
|
225
|
+
target: document.createElement('div'),
|
|
226
|
+
dataSourceKey: 'main',
|
|
227
|
+
collectionName: 'users',
|
|
228
|
+
fieldPath: 'name',
|
|
229
|
+
record: {},
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
expect(window.matchMedia).not.toHaveBeenCalled();
|
|
233
|
+
expect(open).toHaveBeenCalledTimes(1);
|
|
234
|
+
expect(open).toHaveBeenCalledWith(
|
|
235
|
+
expect.objectContaining({
|
|
236
|
+
type: 'drawer',
|
|
237
|
+
title: 'Name',
|
|
238
|
+
placement: 'bottom',
|
|
239
|
+
}),
|
|
240
|
+
);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('keeps an explicit non-mobile layout context ahead of the narrow viewport fallback', async () => {
|
|
244
|
+
engine.registerModels({ QuickEditFormModel });
|
|
245
|
+
addUsersCollection();
|
|
246
|
+
mockMatchMedia(true);
|
|
247
|
+
const open = vi.fn(async () => undefined);
|
|
248
|
+
const target = document.createElement('div');
|
|
249
|
+
engine.context.defineProperty('isMobileLayout', { value: false });
|
|
250
|
+
engine.context.defineProperty('viewer', { value: { open } });
|
|
251
|
+
|
|
252
|
+
await QuickEditFormModel.open({
|
|
253
|
+
flowEngine: engine,
|
|
254
|
+
target,
|
|
255
|
+
dataSourceKey: 'main',
|
|
256
|
+
collectionName: 'users',
|
|
257
|
+
fieldPath: 'name',
|
|
258
|
+
record: {},
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
expect(window.matchMedia).not.toHaveBeenCalled();
|
|
262
|
+
expect(open).toHaveBeenCalledTimes(1);
|
|
263
|
+
expect(open).toHaveBeenCalledWith(
|
|
264
|
+
expect.objectContaining({
|
|
265
|
+
type: 'popover',
|
|
266
|
+
target,
|
|
267
|
+
placement: 'rightTop',
|
|
268
|
+
}),
|
|
269
|
+
);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('keeps dynamic preventClose effective for mobile quick edit drawer containers', async () => {
|
|
273
|
+
engine.registerModels({ QuickEditFormModel });
|
|
274
|
+
addUsersCollection();
|
|
275
|
+
mockMatchMedia(true);
|
|
276
|
+
const originalBeforeClose = vi.fn(async () => true);
|
|
277
|
+
const drawerView = {
|
|
278
|
+
close: vi.fn(),
|
|
279
|
+
update: vi.fn(),
|
|
280
|
+
beforeClose: originalBeforeClose,
|
|
281
|
+
};
|
|
282
|
+
const open = vi.fn(async (config: { content: (view: typeof drawerView) => unknown }) => {
|
|
283
|
+
config.content(drawerView);
|
|
284
|
+
});
|
|
285
|
+
engine.context.defineProperty('viewer', { value: { open } });
|
|
286
|
+
|
|
287
|
+
await QuickEditFormModel.open({
|
|
288
|
+
flowEngine: engine,
|
|
289
|
+
target: document.createElement('div'),
|
|
290
|
+
dataSourceKey: 'main',
|
|
291
|
+
collectionName: 'users',
|
|
292
|
+
fieldPath: 'name',
|
|
293
|
+
record: {},
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
let quickEditModel: QuickEditFormModel | undefined;
|
|
297
|
+
engine.forEachModel((model) => {
|
|
298
|
+
if (model instanceof QuickEditFormModel) {
|
|
299
|
+
quickEditModel = model;
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
quickEditModel?.viewContainer.update?.({ preventClose: true });
|
|
304
|
+
expect(drawerView.update).toHaveBeenCalledWith({ preventClose: true });
|
|
305
|
+
await expect(drawerView.beforeClose?.({ force: false })).resolves.toBe(false);
|
|
306
|
+
expect(originalBeforeClose).not.toHaveBeenCalled();
|
|
307
|
+
|
|
308
|
+
quickEditModel?.viewContainer.update?.({ preventClose: false });
|
|
309
|
+
expect(drawerView.update).toHaveBeenCalledWith({ preventClose: false });
|
|
310
|
+
await expect(drawerView.beforeClose?.({ force: false })).resolves.toBe(true);
|
|
311
|
+
expect(originalBeforeClose).toHaveBeenCalledTimes(1);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('keeps desktop quick edit in the existing right-top popover', async () => {
|
|
315
|
+
engine.registerModels({ QuickEditFormModel });
|
|
316
|
+
mockMatchMedia(false);
|
|
317
|
+
const open = vi.fn(async () => undefined);
|
|
318
|
+
const target = document.createElement('div');
|
|
319
|
+
engine.context.defineProperty('viewer', { value: { open } });
|
|
320
|
+
|
|
321
|
+
await QuickEditFormModel.open({
|
|
322
|
+
flowEngine: engine,
|
|
323
|
+
target,
|
|
324
|
+
dataSourceKey: 'main',
|
|
325
|
+
collectionName: 'users',
|
|
326
|
+
fieldPath: 'name',
|
|
327
|
+
record: {},
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
expect(open).toHaveBeenCalledTimes(1);
|
|
331
|
+
expect(open).toHaveBeenCalledWith(
|
|
332
|
+
expect.objectContaining({
|
|
333
|
+
type: 'popover',
|
|
334
|
+
target,
|
|
335
|
+
placement: 'rightTop',
|
|
336
|
+
styles: {
|
|
337
|
+
body: expect.objectContaining({
|
|
338
|
+
width: 420,
|
|
339
|
+
}),
|
|
340
|
+
},
|
|
341
|
+
}),
|
|
342
|
+
);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it('keeps mobile quick edit content naturally sized with half-window scroll bounds', () => {
|
|
346
|
+
engine.registerModels({ QuickEditFormModel });
|
|
347
|
+
const model = engine.createModel<QuickEditFormModel>({
|
|
348
|
+
use: QuickEditFormModel,
|
|
349
|
+
uid: 'quick-edit-style',
|
|
350
|
+
});
|
|
351
|
+
model.context.defineProperty('isMobileLayout', { value: true });
|
|
352
|
+
|
|
353
|
+
const result = QuickEditFormModel.prototype.render.call(model) as any;
|
|
354
|
+
const formBody = result.props.children[0];
|
|
355
|
+
const actions = result.props.children[1];
|
|
356
|
+
|
|
357
|
+
expect(formBody.props.style).toMatchObject({
|
|
358
|
+
maxHeight: 'calc(50vh - var(--nb-mobile-page-header-height, 40px) - 132px)',
|
|
359
|
+
overflowY: 'auto',
|
|
360
|
+
padding: '8px var(--nb-mobile-page-tabs-content-padding, 12px) 0',
|
|
361
|
+
});
|
|
362
|
+
expect(formBody.props.style.minHeight).toBe(0);
|
|
363
|
+
expect(actions.props.style).toMatchObject({
|
|
364
|
+
justifyContent: 'flex-end',
|
|
365
|
+
padding: '8px var(--nb-mobile-page-tabs-content-padding, 12px) calc(80px + env(safe-area-inset-bottom, 0px))',
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
|
|
50
369
|
it('calls update with filterByTk and merges primary key from ctx.collection/record', async () => {
|
|
51
370
|
// 1) 准备数据源与集合(含主键字段)
|
|
52
371
|
const dsm = engine.context.dataSourceManager;
|
|
@@ -23,6 +23,20 @@ import {
|
|
|
23
23
|
} from '../AssociationFieldModel/recordSelectShared';
|
|
24
24
|
import _ from 'lodash';
|
|
25
25
|
|
|
26
|
+
const mobileSelectSafeAreaPaddingBottom = 'calc(12px + env(safe-area-inset-bottom, 0px))';
|
|
27
|
+
|
|
28
|
+
const mobileSelectConfirmFooterStyle: CSSProperties = {
|
|
29
|
+
paddingBottom: mobileSelectSafeAreaPaddingBottom,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function getMobileSelectListStyle(hasConfirmFooter: boolean): CSSProperties {
|
|
33
|
+
return {
|
|
34
|
+
maxHeight: '60vh',
|
|
35
|
+
overflowY: 'auto',
|
|
36
|
+
paddingBottom: hasConfirmFooter ? undefined : mobileSelectSafeAreaPaddingBottom,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
26
40
|
const labelClassName = css`
|
|
27
41
|
div {
|
|
28
42
|
white-space: nowrap !important;
|
|
@@ -280,13 +294,7 @@ export function MobileLazySelect(props: Readonly<LazySelectProps>) {
|
|
|
280
294
|
showCancelButton
|
|
281
295
|
/>
|
|
282
296
|
</div>
|
|
283
|
-
<div
|
|
284
|
-
style={{
|
|
285
|
-
maxHeight: '60vh',
|
|
286
|
-
overflowY: 'auto',
|
|
287
|
-
}}
|
|
288
|
-
onScroll={handleScroll}
|
|
289
|
-
>
|
|
297
|
+
<div style={getMobileSelectListStyle(isMultiple)} onScroll={handleScroll}>
|
|
290
298
|
<CheckList multiple={isMultiple} value={selectedValueIds} onChange={handleListChange}>
|
|
291
299
|
{realOptions.map((item) => {
|
|
292
300
|
const optionValue = item?.[valueKey];
|
|
@@ -311,9 +319,11 @@ export function MobileLazySelect(props: Readonly<LazySelectProps>) {
|
|
|
311
319
|
)}
|
|
312
320
|
</div>
|
|
313
321
|
{isMultiple && (
|
|
314
|
-
<
|
|
315
|
-
{
|
|
316
|
-
|
|
322
|
+
<div style={mobileSelectConfirmFooterStyle}>
|
|
323
|
+
<Button block color="primary" onClick={handleConfirm} style={{ marginTop: '16px' }}>
|
|
324
|
+
{t('Confirm')}
|
|
325
|
+
</Button>
|
|
326
|
+
</div>
|
|
317
327
|
)}
|
|
318
328
|
</Popup>
|
|
319
329
|
</>
|