@nocobase/client-v2 2.2.0-beta.9 → 2.3.0-alpha.1
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/APIClient.d.ts +1 -3
- package/es/BaseApplication.d.ts +1 -0
- package/es/collection-field-interface/CollectionFieldInterface.d.ts +1 -0
- package/es/collection-field-interface/CollectionFieldInterfaceManager.d.ts +1 -0
- package/es/components/category-tabs/SortableCategoryTabs.d.ts +55 -0
- package/es/components/category-tabs/index.d.ts +9 -0
- package/es/components/form/ScanInput/useCodeScanner.d.ts +2 -1
- package/es/components/form/TypedVariableInput.d.ts +31 -5
- package/es/components/form/filter/CollectionFilter.d.ts +2 -0
- package/es/components/form/filter/CollectionFilterItem.d.ts +11 -1
- package/es/components/form/filter/CollectionFilterPanel.d.ts +2 -0
- package/es/components/form/filter/index.d.ts +2 -0
- package/es/components/form/filter/useFilterActionProps.d.ts +2 -0
- package/es/components/index.d.ts +1 -0
- package/es/flow/actions/index.d.ts +1 -1
- package/es/flow/actions/linkageRules.d.ts +2 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +1 -0
- package/es/flow/components/FieldAssignExactDatePicker.d.ts +1 -0
- package/es/flow/components/FieldAssignValueInput.d.ts +9 -0
- package/es/flow/components/RunJSValueEditor.d.ts +1 -0
- package/es/flow/components/filter/FilterGroup.d.ts +1 -0
- package/es/flow/components/filter/VariableFilterItem.d.ts +6 -1
- package/es/flow/index.d.ts +1 -0
- package/es/flow/models/base/PageModel/PageModel.d.ts +12 -0
- package/es/flow/models/base/PageModel/PageModelTabBar.d.ts +22 -0
- package/es/flow/models/base/PageModel/PageTabModel.d.ts +12 -0
- package/es/flow/models/blocks/form/QuickEditFormModel.d.ts +17 -2
- package/es/flow-compat/fieldValidationConstants.d.ts +1 -1
- package/es/flow-compat/routeTypes.d.ts +1 -0
- package/es/index.d.ts +4 -0
- package/es/index.mjs +153 -118
- package/es/utils/getRouteRuntimeVersion.d.ts +23 -0
- package/es/utils/index.d.ts +1 -0
- package/lib/index.js +162 -127
- package/lib/locale/languageCodes.js +2 -1
- package/package.json +8 -7
- package/src/APIClient.ts +1 -10
- package/src/Application.tsx +4 -0
- package/src/BaseApplication.tsx +11 -6
- package/src/__tests__/app.test.tsx +73 -0
- package/src/__tests__/browserChecker.test.ts +61 -0
- package/src/__tests__/getRouteRuntimeVersion.test.ts +68 -0
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +28 -0
- package/src/collection-field-interface/CollectionFieldInterface.ts +1 -0
- package/src/collection-field-interface/CollectionFieldInterfaceManager.ts +1 -0
- package/src/collection-manager/field-validation.ts +1 -1
- package/src/components/README.md +7 -1
- package/src/components/README.zh-CN.md +6 -1
- package/src/components/category-tabs/SortableCategoryTabs.tsx +210 -0
- package/src/components/category-tabs/index.ts +10 -0
- package/src/components/form/JsonTextArea.tsx +4 -0
- package/src/components/form/ScanInput/CodeScanner.tsx +23 -6
- package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +180 -3
- package/src/components/form/ScanInput/useCodeScanner.ts +170 -5
- package/src/components/form/TypedVariableInput.tsx +452 -101
- package/src/components/form/__tests__/TypedVariableInput.test.tsx +364 -12
- package/src/components/form/filter/CollectionFilter.tsx +4 -0
- package/src/components/form/filter/CollectionFilterItem.tsx +32 -7
- package/src/components/form/filter/CollectionFilterPanel.tsx +4 -0
- package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +38 -0
- package/src/components/form/filter/__tests__/useFilterActionProps.test.tsx +95 -0
- package/src/components/form/filter/index.ts +2 -0
- package/src/components/form/filter/useFilterActionProps.ts +37 -6
- package/src/components/index.ts +1 -0
- package/src/flow/FlowPage.tsx +9 -1
- package/src/flow/__tests__/FlowPage.test.tsx +50 -3
- package/src/flow/__tests__/FlowRoute.test.tsx +2 -2
- package/src/flow/actions/__tests__/linkageRules.tab.test.ts +171 -0
- package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +7 -3
- package/src/flow/actions/index.ts +2 -0
- package/src/flow/actions/linkageRules.tsx +86 -11
- package/src/flow/actions/linkageRulesRefresh.tsx +2 -6
- package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +0 -1
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +253 -6
- package/src/flow/components/FieldAssignExactDatePicker.tsx +25 -11
- package/src/flow/components/FieldAssignValueInput.tsx +91 -20
- package/src/flow/components/FlowRoute.tsx +7 -3
- package/src/flow/components/RunJSValueEditor.tsx +9 -1
- package/src/flow/components/__tests__/FieldAssignValueInput.context.test.tsx +216 -0
- package/src/flow/components/filter/FilterGroup.tsx +33 -5
- package/src/flow/components/filter/VariableFilterItem.tsx +158 -10
- package/src/flow/components/filter/__tests__/FilterGroup.test.tsx +45 -0
- package/src/flow/components/filter/__tests__/VariableFilterItem.leftMetaTree.test.tsx +9 -0
- package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +70 -1
- package/src/flow/index.ts +1 -0
- package/src/flow/models/base/PageModel/PageModel.tsx +387 -53
- package/src/flow/models/base/PageModel/PageModelTabBar.tsx +114 -0
- package/src/flow/models/base/PageModel/PageTabModel.tsx +184 -3
- package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +790 -10
- package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.module-isolation.test.tsx +130 -0
- package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.test.tsx +118 -0
- package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +572 -1
- 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 +189 -36
- package/src/flow/models/blocks/form/__tests__/QuickEditFormModel.quickEdit.test.ts +350 -2
- package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +2 -1
- package/src/flow/models/blocks/table/TableBlockModel.tsx +1 -1
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +2 -1
- package/src/flow/models/fields/VariableFieldFormModel.tsx +3 -3
- package/src/flow/models/fields/__tests__/VariableFieldFormModel.test.tsx +51 -0
- package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +20 -10
- package/src/flow/models/fields/mobile-components/MobileSelect.tsx +24 -12
- package/src/flow-compat/fieldValidationConstants.ts +1 -1
- package/src/flow-compat/routeTypes.ts +1 -0
- package/src/index.ts +4 -0
- package/src/locale/languageCodes.ts +2 -1
- package/src/nocobase-buildin-plugin/index.tsx +12 -0
- package/src/utils/getRouteRuntimeVersion.ts +185 -0
- package/src/utils/index.tsx +1 -0
|
@@ -7,17 +7,365 @@
|
|
|
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';
|
|
11
|
-
import { FlowEngine, SingleRecordResource } from '@nocobase/flow-engine';
|
|
10
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
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
|
+
|
|
58
|
+
it('uses source field context when opening quick edit', async () => {
|
|
59
|
+
engine.registerModels({ QuickEditFormModel });
|
|
60
|
+
engine.context.defineProperty('pageActive', { value: { value: false } });
|
|
61
|
+
engine.context.defineProperty('viewer', { value: { open: vi.fn(async () => undefined) } });
|
|
62
|
+
|
|
63
|
+
const page = engine.createModel<FlowModel>({ use: 'FlowModel', uid: 'page' });
|
|
64
|
+
page.context.defineProperty('pageActive', { value: { value: true } });
|
|
65
|
+
const source = engine.createModel<FlowModel>({ use: 'FlowModel', uid: 'source-field', parentId: page.uid });
|
|
66
|
+
|
|
67
|
+
await QuickEditFormModel.open({
|
|
68
|
+
flowEngine: engine,
|
|
69
|
+
target: document.createElement('div'),
|
|
70
|
+
dataSourceKey: 'main',
|
|
71
|
+
collectionName: 'users',
|
|
72
|
+
fieldPath: 'name',
|
|
73
|
+
record: {},
|
|
74
|
+
sourceFieldModelUid: source.uid,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
let quickEditModel: QuickEditFormModel | undefined;
|
|
78
|
+
engine.forEachModel((model) => {
|
|
79
|
+
if (model instanceof QuickEditFormModel) {
|
|
80
|
+
quickEditModel = model;
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
expect(quickEditModel?.context.pageActive.value).toBe(true);
|
|
85
|
+
});
|
|
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
|
+
|
|
21
369
|
it('calls update with filterByTk and merges primary key from ctx.collection/record', async () => {
|
|
22
370
|
// 1) 准备数据源与集合(含主键字段)
|
|
23
371
|
const dsm = engine.context.dataSourceManager;
|
|
@@ -121,10 +121,11 @@ const Columns = observer<any>(({ record, model, index }) => {
|
|
|
121
121
|
fork.context.defineProperty('recordIndex', {
|
|
122
122
|
get: () => index,
|
|
123
123
|
});
|
|
124
|
+
const rendererKey = `${fork.uid}:${fork.forkId}`;
|
|
124
125
|
const renderer = (
|
|
125
126
|
<FlowModelRenderer
|
|
126
127
|
showFlowSettings={{ showBorder: false, toolbarPosition: 'above' }}
|
|
127
|
-
key={
|
|
128
|
+
key={rendererKey}
|
|
128
129
|
model={fork}
|
|
129
130
|
inputArgs={record}
|
|
130
131
|
fallback={<Skeleton.Button size="small" />}
|
|
@@ -310,7 +310,7 @@ export class TableBlockModel extends CollectionBlockModel<TableBlockModelStructu
|
|
|
310
310
|
transform: translateY(-50%);
|
|
311
311
|
}
|
|
312
312
|
&:hover {
|
|
313
|
-
|
|
313
|
+
box-shadow: inset 0 0 0 9999px rgba(24, 144, 255, 0.1);
|
|
314
314
|
}
|
|
315
315
|
&:hover .edit-icon {
|
|
316
316
|
display: inline-flex;
|
|
@@ -102,7 +102,7 @@ const RenderCell = observer<any>((props) => {
|
|
|
102
102
|
transform: translateY(-50%);
|
|
103
103
|
}
|
|
104
104
|
&:hover {
|
|
105
|
-
|
|
105
|
+
box-shadow: inset 0 0 0 9999px rgba(24, 144, 255, 0.1);
|
|
106
106
|
}
|
|
107
107
|
&:hover .edit-icon {
|
|
108
108
|
display: inline-flex;
|
|
@@ -126,6 +126,7 @@ const RenderCell = observer<any>((props) => {
|
|
|
126
126
|
fieldPath: dataIndex,
|
|
127
127
|
record: record,
|
|
128
128
|
fieldProps: { ...columnModel.props, ...columnModel.subModels.field.props },
|
|
129
|
+
sourceFieldModelUid: columnModel.subModels.field.uid,
|
|
129
130
|
onOk: (values) => {
|
|
130
131
|
record[dataIndex] = values[dataIndex];
|
|
131
132
|
// 仅重渲染单元格
|
|
@@ -48,12 +48,12 @@ export class VariableFieldFormModel extends FlowModel {
|
|
|
48
48
|
<FormProvider form={this.form}>
|
|
49
49
|
<FormLayout layout={'vertical'}>
|
|
50
50
|
{this.mapSubModels('fields', (field) => {
|
|
51
|
-
//
|
|
51
|
+
// Avoid conflicting with the DOM nodeName property when rendering the temporary input.
|
|
52
52
|
const init = field?.getStepParams?.('fieldSettings', 'init') || {};
|
|
53
53
|
const fp = init?.fieldPath as string | undefined;
|
|
54
54
|
if (fp) {
|
|
55
|
-
const namePath = fp
|
|
56
|
-
const toSet:
|
|
55
|
+
const namePath = (fp === 'nodeName' ? '__nb_variable_field_nodeName' : fp).split('.');
|
|
56
|
+
const toSet: Record<string, unknown> = {};
|
|
57
57
|
if (!field?.props?.id) toSet.id = namePath;
|
|
58
58
|
if (!field?.props?.name) toSet.name = namePath;
|
|
59
59
|
if (Object.keys(toSet).length) field?.setProps?.(toSet);
|
|
@@ -0,0 +1,51 @@
|
|
|
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 { FlowEngine, FlowEngineProvider } from '@nocobase/flow-engine';
|
|
11
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
12
|
+
import React from 'react';
|
|
13
|
+
import { describe, expect, it } from 'vitest';
|
|
14
|
+
import { InputFieldModel } from '../InputFieldModel';
|
|
15
|
+
import { VariableFieldFormModel } from '../VariableFieldFormModel';
|
|
16
|
+
|
|
17
|
+
describe('VariableFieldFormModel', () => {
|
|
18
|
+
it('uses DOM-safe name and id for temporary controls created from reserved form property names', async () => {
|
|
19
|
+
const engine = new FlowEngine();
|
|
20
|
+
engine.registerModels({ InputFieldModel, VariableFieldFormModel });
|
|
21
|
+
const model = engine.createModel<VariableFieldFormModel>({
|
|
22
|
+
use: 'VariableFieldFormModel',
|
|
23
|
+
subModels: {
|
|
24
|
+
fields: [
|
|
25
|
+
{
|
|
26
|
+
use: 'InputFieldModel',
|
|
27
|
+
stepParams: {
|
|
28
|
+
fieldSettings: {
|
|
29
|
+
init: {
|
|
30
|
+
fieldPath: 'nodeName',
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
const field = model.subModels.fields[0];
|
|
39
|
+
|
|
40
|
+
render(<FlowEngineProvider engine={engine}>{model.render()}</FlowEngineProvider>);
|
|
41
|
+
|
|
42
|
+
const input = await screen.findByRole('textbox');
|
|
43
|
+
await waitFor(() => {
|
|
44
|
+
expect(field.props.name).toEqual(['__nb_variable_field_nodeName']);
|
|
45
|
+
});
|
|
46
|
+
expect(input).toHaveAttribute('name', '__nb_variable_field_nodeName');
|
|
47
|
+
expect(input).toHaveAttribute('id', '__nb_variable_field_nodeName');
|
|
48
|
+
expect(field.props.id).toEqual(['__nb_variable_field_nodeName']);
|
|
49
|
+
expect(field.getStepParams('fieldSettings', 'init')).toEqual({ fieldPath: 'nodeName' });
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -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
|
</>
|
|
@@ -12,8 +12,23 @@ import { Select } from 'antd';
|
|
|
12
12
|
import { Button, CheckList, Popup, SearchBar } from 'antd-mobile';
|
|
13
13
|
import React, { useEffect, useMemo, useState } from 'react';
|
|
14
14
|
|
|
15
|
+
const mobileSelectSafeAreaPaddingBottom = 'calc(12px + env(safe-area-inset-bottom, 0px))';
|
|
16
|
+
|
|
17
|
+
const mobileSelectConfirmFooterStyle: React.CSSProperties = {
|
|
18
|
+
paddingBottom: mobileSelectSafeAreaPaddingBottom,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function getMobileSelectListStyle(hasConfirmFooter: boolean): React.CSSProperties {
|
|
22
|
+
return {
|
|
23
|
+
maxHeight: '60vh',
|
|
24
|
+
overflowY: 'auto',
|
|
25
|
+
paddingBottom: hasConfirmFooter ? undefined : mobileSelectSafeAreaPaddingBottom,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
15
29
|
export function MobileSelect(props) {
|
|
16
30
|
const { value, displayValue, onChange, onChangeComplete, disabled, options = [], mode } = props;
|
|
31
|
+
const isMultiple = ['multiple', 'tags'].includes(mode);
|
|
17
32
|
const ctx = useFlowModelContext();
|
|
18
33
|
const t = ctx.t;
|
|
19
34
|
const [visible, setVisible] = useState(false);
|
|
@@ -64,17 +79,12 @@ export function MobileSelect(props) {
|
|
|
64
79
|
<div style={{ margin: '10px' }}>
|
|
65
80
|
<SearchBar placeholder={t('search')} value={searchText} onChange={(v) => setSearchText(v)} showCancelButton />
|
|
66
81
|
</div>
|
|
67
|
-
<div
|
|
68
|
-
style={{
|
|
69
|
-
maxHeight: '60vh',
|
|
70
|
-
overflowY: 'auto',
|
|
71
|
-
}}
|
|
72
|
-
>
|
|
82
|
+
<div style={getMobileSelectListStyle(isMultiple)}>
|
|
73
83
|
<CheckList
|
|
74
|
-
multiple={
|
|
84
|
+
multiple={isMultiple}
|
|
75
85
|
value={Array.isArray(selected) ? selected : [selected]}
|
|
76
86
|
onChange={(val) => {
|
|
77
|
-
if (
|
|
87
|
+
if (isMultiple) {
|
|
78
88
|
setSelected(val);
|
|
79
89
|
} else {
|
|
80
90
|
setSelected(val[0]);
|
|
@@ -91,10 +101,12 @@ export function MobileSelect(props) {
|
|
|
91
101
|
))}
|
|
92
102
|
</CheckList>
|
|
93
103
|
</div>
|
|
94
|
-
{
|
|
95
|
-
<
|
|
96
|
-
{
|
|
97
|
-
|
|
104
|
+
{isMultiple && (
|
|
105
|
+
<div style={mobileSelectConfirmFooterStyle}>
|
|
106
|
+
<Button block color="primary" onClick={handleConfirm} style={{ marginTop: '16px' }}>
|
|
107
|
+
{t('Confirm')}
|
|
108
|
+
</Button>
|
|
109
|
+
</div>
|
|
98
110
|
)}
|
|
99
111
|
</Popup>
|
|
100
112
|
</>
|
package/src/index.ts
CHANGED
|
@@ -30,6 +30,8 @@ export * from './layout-manager';
|
|
|
30
30
|
export * from './hooks';
|
|
31
31
|
export { default as languageCodes } from './locale/languageCodes';
|
|
32
32
|
export * from './nocobase-buildin-plugin';
|
|
33
|
+
export { getRouteRuntimeVersion } from './utils/getRouteRuntimeVersion';
|
|
34
|
+
export type { RouteRuntimeVersion } from './utils/getRouteRuntimeVersion';
|
|
33
35
|
export * from './collection-field-interface/CollectionFieldInterface';
|
|
34
36
|
export * from './collection-field-interface/CollectionFieldInterfaceManager';
|
|
35
37
|
export * from './collection-manager/field-configure';
|
|
@@ -40,6 +42,8 @@ export * from './collection-manager/template-fields';
|
|
|
40
42
|
export * from './data-source';
|
|
41
43
|
export * from './entry-actions';
|
|
42
44
|
export * from './flow';
|
|
45
|
+
export { CodeEditorExtension } from './flow/components/code-editor/extension';
|
|
46
|
+
export type { CodeEditorExtra, CodeEditorExtraRegistry, EditorRef } from './flow/components/code-editor/types';
|
|
43
47
|
export {
|
|
44
48
|
DEFAULT_DATA_SOURCE_KEY,
|
|
45
49
|
IconPicker,
|
|
@@ -73,7 +73,8 @@ export const languageCodes: Record<string, LocaleOptions> = {
|
|
|
73
73
|
'tk-TK': { label: 'Turkmen' },
|
|
74
74
|
'tr-TR': { label: 'Türkçe' },
|
|
75
75
|
'uk-UA': { label: 'Українська' },
|
|
76
|
-
'ur-PK': { label: '
|
|
76
|
+
'ur-PK': { label: 'اردو' },
|
|
77
|
+
'uz-UZ': { label: 'Oʻzbekcha' },
|
|
77
78
|
'vi-VN': { label: 'Tiếng Việt' },
|
|
78
79
|
'zh-CN': { label: '简体中文' },
|
|
79
80
|
'zh-HK': { label: '繁體中文(香港)' },
|
|
@@ -240,6 +240,16 @@ const CurrentUserProvider: FC = ({ children }) => {
|
|
|
240
240
|
return;
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
+
try {
|
|
244
|
+
await app.apiClient.auth.syncCookies();
|
|
245
|
+
} catch {
|
|
246
|
+
// Cookie bootstrap is best-effort; auth:check remains the source of truth for the current page load.
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (!mounted) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
243
253
|
const userMeta = createCollectionContextMeta(
|
|
244
254
|
() => app.flowEngine.context.dataSourceManager?.getDataSource('main')?.getCollection('users') || null,
|
|
245
255
|
app.flowEngine.translate('Current user'),
|
|
@@ -298,6 +308,8 @@ const CurrentUserProvider: FC = ({ children }) => {
|
|
|
298
308
|
return <CurrentUserContext.Provider value={contextValue}>{children}</CurrentUserContext.Provider>;
|
|
299
309
|
};
|
|
300
310
|
|
|
311
|
+
CurrentUserProvider.displayName = 'CurrentUserProvider';
|
|
312
|
+
|
|
301
313
|
const RootRedirect: FC = () => {
|
|
302
314
|
const app = useApp<Application>();
|
|
303
315
|
const hasToken = !!app?.apiClient?.auth?.token;
|