@nocobase/client-v2 2.3.0-alpha.1 → 3.0.0-alpha.2
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/acl/ACLProvider.d.ts +10 -0
- package/es/authRedirect.d.ts +5 -0
- package/es/components/AttachmentUpload.d.ts +51 -0
- package/es/components/form/ScanInput/useCodeScanner.d.ts +2 -2
- package/es/components/index.d.ts +1 -0
- package/es/flow/actions/linkageRules.d.ts +1 -0
- package/es/flow/admin-shell/admin-layout/NocoBaseLogo.d.ts +10 -0
- package/es/flow/components/FlowRoute.d.ts +2 -2
- package/es/flow/internal/registerDeviceTypeContext.d.ts +10 -0
- package/es/flow/models/base/ActionModelCore.d.ts +4 -2
- package/es/flow/models/base/BlockGridModel.d.ts +3 -0
- package/es/flow/models/blocks/table/TableColumnModel.d.ts +1 -0
- package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +15 -7
- package/es/index.d.ts +4 -1
- package/es/index.mjs +139 -125
- package/es/nocobase-buildin-plugin/currentUserAuthStatus.d.ts +11 -0
- package/es/nocobase-buildin-plugin/index.d.ts +9 -0
- package/es/settings-app/SettingsApplication.d.ts +18 -0
- package/es/settings-app/SettingsBrand.d.ts +16 -0
- package/es/settings-app/SettingsBuildInPlugin.d.ts +9 -0
- package/es/settings-app/SettingsDocumentRedirect.d.ts +10 -0
- package/es/settings-app/SettingsGroupNav.d.ts +17 -0
- package/es/settings-app/SettingsPluginSettingsManager.d.ts +14 -0
- package/es/settings-app/SettingsRouterManager.d.ts +15 -0
- package/es/settings-app/SettingsSearch.d.ts +17 -0
- package/es/settings-app/SettingsShell.d.ts +10 -0
- package/es/settings-app/isSettingsApp.d.ts +18 -0
- package/es/settings-app/runtimePaths.d.ts +18 -0
- package/es/settings-app/settingsDocumentPath.d.ts +14 -0
- package/es/settings-app/settingsTheme.d.ts +87 -0
- package/es/settings-app/useSettingsThemeConfig.d.ts +22 -0
- package/es/settings-center/AdminSettingsLayout.d.ts +2 -2
- package/es/settings-center/groups.d.ts +66 -0
- package/es/settings-center/useSettingsGroups.d.ts +36 -0
- package/es/settings-center/useSettingsSearch.d.ts +35 -0
- package/es/settings-center/utils.d.ts +24 -0
- package/es/theme/index.d.ts +1 -0
- package/es/theme/itemActive.d.ts +24 -0
- package/es/theme/type.d.ts +9 -0
- package/lib/index.js +141 -127
- package/lib/locale/languageCodes.js +1 -0
- package/package.json +8 -7
- package/src/__tests__/authRedirect.test.ts +137 -0
- package/src/__tests__/browserChecker.test.ts +124 -6
- package/src/__tests__/mockSettingsApplication.ts +29 -0
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +51 -10
- package/src/__tests__/plugin-manager.test.tsx +7 -6
- package/src/__tests__/settings-application.test.ts +164 -0
- package/src/__tests__/settings-auth-routes.test.ts +71 -0
- package/src/__tests__/settings-center.test.tsx +38 -37
- package/src/__tests__/settings-layout-root.test.tsx +260 -0
- package/src/__tests__/settings-runtime-paths.test.ts +99 -0
- package/src/__tests__/settings-shell.test.tsx +200 -0
- package/src/acl/ACLProvider.tsx +21 -0
- package/src/authRedirect.ts +72 -3
- package/src/components/AttachmentUpload.tsx +275 -0
- package/src/components/form/ScanInput/CodeScanner.tsx +61 -34
- package/src/components/form/ScanInput/__tests__/CodeScanner.test.tsx +101 -0
- package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +114 -18
- package/src/components/form/ScanInput/useCodeScanner.ts +166 -23
- package/src/components/index.ts +1 -0
- package/src/flow/__tests__/FlowRoute.test.tsx +126 -8
- package/src/flow/__tests__/PluginFlowEngine.test.ts +58 -0
- package/src/flow/actions/__tests__/actionLinkageRules.forkProps.test.ts +314 -0
- package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +102 -0
- package/src/flow/actions/linkageRules.tsx +26 -6
- package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +1 -56
- package/src/flow/admin-shell/admin-layout/NocoBaseLogo.tsx +77 -0
- package/src/flow/admin-shell/admin-layout/TopbarActionsBar.tsx +37 -14
- package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +77 -13
- package/src/flow/common/Markdown/style.ts +4 -0
- package/src/flow/components/BlockItemCard.tsx +2 -2
- package/src/flow/components/FlowRoute.tsx +71 -29
- package/src/flow/index.ts +2 -0
- package/src/flow/internal/registerDeviceTypeContext.ts +39 -0
- package/src/flow/models/base/ActionModelCore.tsx +11 -2
- package/src/flow/models/base/BlockGridModel.tsx +26 -0
- package/src/flow/models/base/CollectionBlockModel.tsx +6 -2
- package/src/flow/models/base/PageModel/PageTabModel.tsx +67 -17
- package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +509 -12
- package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +49 -0
- package/src/flow/models/base/__tests__/BlockGridModel.selectSceneActivation.test.ts +124 -0
- package/src/flow/models/base/__tests__/CollectionBlockModel.addAppends.test.ts +41 -0
- package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +27 -3
- package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +32 -4
- package/src/flow/models/blocks/js-block/JSBlock.tsx +1 -1
- package/src/flow/models/blocks/table/TableBlockModel.tsx +3 -1
- package/src/flow/models/blocks/table/TableColumnModel.tsx +30 -1
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.quickEditRefresh.test.ts +12 -0
- package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +33 -0
- package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +149 -0
- package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +149 -113
- package/src/flow/models/fields/JsonFieldModel.tsx +31 -8
- package/src/flow/models/fields/__tests__/JsonFieldModel.test.ts +82 -0
- package/src/flow/models/topbar/TopbarActionModel.tsx +52 -180
- package/src/flow/utils/dataScopeFormValueClear.ts +4 -3
- package/src/index.ts +10 -1
- package/src/locale/languageCodes.ts +1 -0
- package/src/nocobase-buildin-plugin/currentUserAuthStatus.ts +58 -0
- package/src/nocobase-buildin-plugin/index.tsx +99 -71
- package/src/settings-app/SettingsApplication.ts +31 -0
- package/src/settings-app/SettingsBrand.tsx +108 -0
- package/src/settings-app/SettingsBuildInPlugin.ts +10 -0
- package/src/settings-app/SettingsDocumentRedirect.tsx +26 -0
- package/src/settings-app/SettingsGroupNav.tsx +124 -0
- package/src/settings-app/SettingsPluginSettingsManager.ts +38 -0
- package/src/settings-app/SettingsRouterManager.ts +92 -0
- package/src/settings-app/SettingsSearch.tsx +238 -0
- package/src/settings-app/SettingsShell.tsx +167 -0
- package/src/settings-app/isSettingsApp.ts +21 -0
- package/src/settings-app/runtimePaths.ts +104 -0
- package/src/settings-app/settingsDocumentPath.ts +66 -0
- package/src/settings-app/settingsTheme.ts +301 -0
- package/src/settings-app/useSettingsThemeConfig.ts +100 -0
- package/src/settings-center/AdminSettingsLayout.tsx +122 -141
- package/src/settings-center/SystemSettingsPage.tsx +4 -170
- package/src/settings-center/groups.ts +108 -0
- package/src/settings-center/useSettingsGroups.ts +105 -0
- package/src/settings-center/useSettingsSearch.ts +173 -0
- package/src/settings-center/utils.tsx +50 -1
- package/src/theme/index.tsx +1 -0
- package/src/theme/itemActive.ts +31 -0
- package/src/theme/type.ts +10 -0
|
@@ -88,6 +88,51 @@ vi.mock('ahooks', async (importOriginal) => {
|
|
|
88
88
|
};
|
|
89
89
|
});
|
|
90
90
|
|
|
91
|
+
type RootPageTabModelClass = typeof import('../PageTabModel').RootPageTabModel;
|
|
92
|
+
|
|
93
|
+
type TestRoute = {
|
|
94
|
+
schemaUid?: string;
|
|
95
|
+
options?: Record<string, unknown>;
|
|
96
|
+
} & Record<string, unknown>;
|
|
97
|
+
|
|
98
|
+
type RootPageTabModelTestOptions = {
|
|
99
|
+
props?: {
|
|
100
|
+
route?: TestRoute;
|
|
101
|
+
};
|
|
102
|
+
stepParams?: {
|
|
103
|
+
pageTabSettings?: {
|
|
104
|
+
tab?: {
|
|
105
|
+
title?: string;
|
|
106
|
+
icon?: string;
|
|
107
|
+
documentTitle?: string;
|
|
108
|
+
};
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
context: {
|
|
112
|
+
api: {
|
|
113
|
+
request: ReturnType<typeof vi.fn>;
|
|
114
|
+
};
|
|
115
|
+
routeRepository?: {
|
|
116
|
+
getRouteBySchemaUid?: (schemaUid: string) => TestRoute | undefined;
|
|
117
|
+
refreshAccessible?: () => Promise<unknown>;
|
|
118
|
+
};
|
|
119
|
+
refreshDesktopRoutes?: () => Promise<unknown>;
|
|
120
|
+
logger?: {
|
|
121
|
+
warn?: (context: { err: unknown }, message: string) => void;
|
|
122
|
+
};
|
|
123
|
+
t: (value: string) => string;
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
type RootPageTabModelTestConstructor = new (
|
|
128
|
+
options: RootPageTabModelTestOptions,
|
|
129
|
+
) => InstanceType<RootPageTabModelClass>;
|
|
130
|
+
|
|
131
|
+
function createRootPageTabModel(Model: RootPageTabModelClass, options: RootPageTabModelTestOptions) {
|
|
132
|
+
const TestModel = Model as unknown as RootPageTabModelTestConstructor;
|
|
133
|
+
return new TestModel(options);
|
|
134
|
+
}
|
|
135
|
+
|
|
91
136
|
describe('PageTabModel', () => {
|
|
92
137
|
beforeEach(() => {
|
|
93
138
|
vi.resetModules();
|
|
@@ -149,7 +194,7 @@ describe('PageTabModel', () => {
|
|
|
149
194
|
it('should persist tab documentTitle to route options on save', async () => {
|
|
150
195
|
const { RootPageTabModel } = await import('../PageTabModel');
|
|
151
196
|
const request = vi.fn().mockResolvedValue({});
|
|
152
|
-
const model =
|
|
197
|
+
const model = createRootPageTabModel(RootPageTabModel, {
|
|
153
198
|
props: {
|
|
154
199
|
route: {
|
|
155
200
|
schemaUid: 'tab-1',
|
|
@@ -168,7 +213,7 @@ describe('PageTabModel', () => {
|
|
|
168
213
|
api: { request },
|
|
169
214
|
t: (value: string) => value,
|
|
170
215
|
},
|
|
171
|
-
}
|
|
216
|
+
});
|
|
172
217
|
|
|
173
218
|
await model.save();
|
|
174
219
|
|
|
@@ -177,6 +222,413 @@ describe('PageTabModel', () => {
|
|
|
177
222
|
expect(call.data.options.documentTitle).toBe('Tab doc title');
|
|
178
223
|
});
|
|
179
224
|
|
|
225
|
+
it('should update a persisted tab route by id with only editable fields and latest options', async () => {
|
|
226
|
+
const { RootPageTabModel } = await import('../PageTabModel');
|
|
227
|
+
const request = vi.fn().mockResolvedValue({});
|
|
228
|
+
const model = createRootPageTabModel(RootPageTabModel, {
|
|
229
|
+
props: {
|
|
230
|
+
route: {
|
|
231
|
+
id: 123,
|
|
232
|
+
parentId: 456,
|
|
233
|
+
schemaUid: 'tab-1',
|
|
234
|
+
type: 'tabs',
|
|
235
|
+
sort: 1,
|
|
236
|
+
hidden: true,
|
|
237
|
+
tabSchemaName: 'tab-name-1',
|
|
238
|
+
hideInMenu: false,
|
|
239
|
+
enableTabs: false,
|
|
240
|
+
roles: [{ name: 'admin', title: 'Admin' }],
|
|
241
|
+
createdAt: '2026-07-16T00:00:00.000Z',
|
|
242
|
+
updatedAt: '2026-07-16T01:00:00.000Z',
|
|
243
|
+
options: {
|
|
244
|
+
badge: {
|
|
245
|
+
showZero: false,
|
|
246
|
+
},
|
|
247
|
+
pluginState: 'stale',
|
|
248
|
+
removedPluginState: 'stale-only',
|
|
249
|
+
flowRegistry: { stale: true },
|
|
250
|
+
documentTitle: 'Stale document title',
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
stepParams: {
|
|
255
|
+
pageTabSettings: {
|
|
256
|
+
tab: {
|
|
257
|
+
title: 'Renamed tab',
|
|
258
|
+
icon: 'TableOutlined',
|
|
259
|
+
documentTitle: 'Current document title',
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
context: {
|
|
264
|
+
api: { request },
|
|
265
|
+
routeRepository: {
|
|
266
|
+
getRouteBySchemaUid: vi.fn(() => ({
|
|
267
|
+
schemaUid: 'tab-1',
|
|
268
|
+
options: {
|
|
269
|
+
badge: {
|
|
270
|
+
showZero: true,
|
|
271
|
+
},
|
|
272
|
+
pluginState: 'fresh',
|
|
273
|
+
flowRegistry: { fresh: true },
|
|
274
|
+
documentTitle: 'Fresh document title',
|
|
275
|
+
},
|
|
276
|
+
})),
|
|
277
|
+
},
|
|
278
|
+
t: (value: string) => value,
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
await model.save();
|
|
283
|
+
|
|
284
|
+
const call = request.mock.calls[0][0];
|
|
285
|
+
expect(call).toEqual({
|
|
286
|
+
method: 'post',
|
|
287
|
+
url: 'desktopRoutes:update?filter[id]=123',
|
|
288
|
+
data: {
|
|
289
|
+
schemaUid: 'tab-1',
|
|
290
|
+
title: 'Renamed tab',
|
|
291
|
+
icon: 'TableOutlined',
|
|
292
|
+
options: {
|
|
293
|
+
badge: {
|
|
294
|
+
showZero: true,
|
|
295
|
+
},
|
|
296
|
+
pluginState: 'fresh',
|
|
297
|
+
flowRegistry: {},
|
|
298
|
+
documentTitle: 'Current document title',
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
});
|
|
302
|
+
expect(Object.keys(call.data).sort()).toEqual(['icon', 'options', 'schemaUid', 'title']);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it('should fall back to model route options when the current route is unavailable', async () => {
|
|
306
|
+
const { RootPageTabModel } = await import('../PageTabModel');
|
|
307
|
+
const request = vi.fn().mockResolvedValue({});
|
|
308
|
+
const model = createRootPageTabModel(RootPageTabModel, {
|
|
309
|
+
props: {
|
|
310
|
+
route: {
|
|
311
|
+
id: 123,
|
|
312
|
+
parentId: 456,
|
|
313
|
+
schemaUid: 'tab-1',
|
|
314
|
+
type: 'tabs',
|
|
315
|
+
sort: 1,
|
|
316
|
+
hidden: true,
|
|
317
|
+
options: {
|
|
318
|
+
badge: {
|
|
319
|
+
showZero: true,
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
stepParams: {
|
|
325
|
+
pageTabSettings: {
|
|
326
|
+
tab: {
|
|
327
|
+
title: 'Renamed tab',
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
context: {
|
|
332
|
+
api: { request },
|
|
333
|
+
routeRepository: {
|
|
334
|
+
getRouteBySchemaUid: vi.fn(() => undefined),
|
|
335
|
+
},
|
|
336
|
+
t: (value: string) => value,
|
|
337
|
+
},
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
await model.save();
|
|
341
|
+
|
|
342
|
+
const call = request.mock.calls[0][0];
|
|
343
|
+
expect(call).toEqual({
|
|
344
|
+
method: 'post',
|
|
345
|
+
url: 'desktopRoutes:update?filter[id]=123',
|
|
346
|
+
data: {
|
|
347
|
+
schemaUid: 'tab-1',
|
|
348
|
+
title: 'Renamed tab',
|
|
349
|
+
icon: undefined,
|
|
350
|
+
options: {
|
|
351
|
+
badge: {
|
|
352
|
+
showZero: true,
|
|
353
|
+
},
|
|
354
|
+
flowRegistry: {},
|
|
355
|
+
documentTitle: undefined,
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it('should await a route refresh after the request before a subsequent persisted tab save', async () => {
|
|
362
|
+
const { RootPageTabModel } = await import('../PageTabModel');
|
|
363
|
+
const events: string[] = [];
|
|
364
|
+
let resolveFirstRefresh: () => void = () => {};
|
|
365
|
+
const firstRefreshDone = new Promise<void>((resolve) => {
|
|
366
|
+
resolveFirstRefresh = resolve;
|
|
367
|
+
});
|
|
368
|
+
let currentRoute = {
|
|
369
|
+
schemaUid: 'tab-1',
|
|
370
|
+
options: {
|
|
371
|
+
pluginState: 'before-save',
|
|
372
|
+
},
|
|
373
|
+
};
|
|
374
|
+
let requestCall = 0;
|
|
375
|
+
const request = vi.fn(async () => {
|
|
376
|
+
requestCall += 1;
|
|
377
|
+
events.push(`request:${requestCall}`);
|
|
378
|
+
if (requestCall === 1) {
|
|
379
|
+
return {
|
|
380
|
+
data: {
|
|
381
|
+
data: [
|
|
382
|
+
{
|
|
383
|
+
id: 123,
|
|
384
|
+
schemaUid: 'tab-1',
|
|
385
|
+
options: {
|
|
386
|
+
pluginState: 'after-save',
|
|
387
|
+
serverHookState: true,
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
],
|
|
391
|
+
},
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
return {};
|
|
395
|
+
});
|
|
396
|
+
let refreshCall = 0;
|
|
397
|
+
const refreshAccessible = vi.fn(async () => {
|
|
398
|
+
refreshCall += 1;
|
|
399
|
+
events.push(`refresh:${refreshCall}:start`);
|
|
400
|
+
if (refreshCall === 1) {
|
|
401
|
+
await firstRefreshDone;
|
|
402
|
+
currentRoute = {
|
|
403
|
+
schemaUid: 'tab-1',
|
|
404
|
+
options: {
|
|
405
|
+
pluginState: 'after-save',
|
|
406
|
+
serverHookState: true,
|
|
407
|
+
},
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
events.push(`refresh:${refreshCall}:end`);
|
|
411
|
+
});
|
|
412
|
+
const model = createRootPageTabModel(RootPageTabModel, {
|
|
413
|
+
props: {
|
|
414
|
+
route: {
|
|
415
|
+
id: 123,
|
|
416
|
+
schemaUid: 'tab-1',
|
|
417
|
+
},
|
|
418
|
+
},
|
|
419
|
+
stepParams: {
|
|
420
|
+
pageTabSettings: {
|
|
421
|
+
tab: {
|
|
422
|
+
title: 'Renamed tab',
|
|
423
|
+
},
|
|
424
|
+
},
|
|
425
|
+
},
|
|
426
|
+
context: {
|
|
427
|
+
api: { request },
|
|
428
|
+
routeRepository: {
|
|
429
|
+
getRouteBySchemaUid: vi.fn(() => currentRoute),
|
|
430
|
+
refreshAccessible,
|
|
431
|
+
},
|
|
432
|
+
t: (value: string) => value,
|
|
433
|
+
},
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
let firstSaveResolved = false;
|
|
437
|
+
const firstSave = model.save().then(() => {
|
|
438
|
+
firstSaveResolved = true;
|
|
439
|
+
events.push('save:1:end');
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
await vi.waitFor(() => expect(refreshAccessible).toHaveBeenCalledTimes(1));
|
|
443
|
+
expect(events.slice(0, 2)).toEqual(['request:1', 'refresh:1:start']);
|
|
444
|
+
await Promise.resolve();
|
|
445
|
+
expect(firstSaveResolved).toBe(false);
|
|
446
|
+
|
|
447
|
+
resolveFirstRefresh();
|
|
448
|
+
await firstSave;
|
|
449
|
+
expect(events.indexOf('request:1')).toBeLessThan(events.indexOf('refresh:1:start'));
|
|
450
|
+
expect(events.indexOf('refresh:1:end')).toBeLessThan(events.indexOf('save:1:end'));
|
|
451
|
+
|
|
452
|
+
await model.save();
|
|
453
|
+
|
|
454
|
+
expect(refreshAccessible).toHaveBeenCalledTimes(2);
|
|
455
|
+
expect(request.mock.calls[1][0].data.options).toMatchObject({
|
|
456
|
+
pluginState: 'after-save',
|
|
457
|
+
serverHookState: true,
|
|
458
|
+
});
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it('should serialize route refreshes when new tabs are saved concurrently', async () => {
|
|
462
|
+
const { RootPageTabModel } = await import('../PageTabModel');
|
|
463
|
+
let releaseFirstRefresh: () => void = () => {};
|
|
464
|
+
const firstRefreshDone = new Promise<void>((resolve) => {
|
|
465
|
+
releaseFirstRefresh = resolve;
|
|
466
|
+
});
|
|
467
|
+
let refreshCall = 0;
|
|
468
|
+
let activeRefreshes = 0;
|
|
469
|
+
let maxActiveRefreshes = 0;
|
|
470
|
+
const refreshAccessible = vi.fn(async () => {
|
|
471
|
+
refreshCall += 1;
|
|
472
|
+
activeRefreshes += 1;
|
|
473
|
+
maxActiveRefreshes = Math.max(maxActiveRefreshes, activeRefreshes);
|
|
474
|
+
if (refreshCall === 1) {
|
|
475
|
+
await firstRefreshDone;
|
|
476
|
+
}
|
|
477
|
+
activeRefreshes -= 1;
|
|
478
|
+
});
|
|
479
|
+
const request = vi
|
|
480
|
+
.fn()
|
|
481
|
+
.mockResolvedValueOnce({
|
|
482
|
+
data: {
|
|
483
|
+
data: {
|
|
484
|
+
id: 101,
|
|
485
|
+
schemaUid: 'tab-1',
|
|
486
|
+
},
|
|
487
|
+
},
|
|
488
|
+
})
|
|
489
|
+
.mockResolvedValueOnce({
|
|
490
|
+
data: {
|
|
491
|
+
data: {
|
|
492
|
+
id: 102,
|
|
493
|
+
schemaUid: 'tab-2',
|
|
494
|
+
},
|
|
495
|
+
},
|
|
496
|
+
});
|
|
497
|
+
const routeRepository = {
|
|
498
|
+
refreshAccessible,
|
|
499
|
+
};
|
|
500
|
+
const createModel = (schemaUid: string, title: string) =>
|
|
501
|
+
createRootPageTabModel(RootPageTabModel, {
|
|
502
|
+
props: {
|
|
503
|
+
route: {
|
|
504
|
+
parentId: 999,
|
|
505
|
+
schemaUid,
|
|
506
|
+
tabSchemaName: `${schemaUid}-name`,
|
|
507
|
+
type: 'tabs',
|
|
508
|
+
},
|
|
509
|
+
},
|
|
510
|
+
stepParams: {
|
|
511
|
+
pageTabSettings: {
|
|
512
|
+
tab: {
|
|
513
|
+
title,
|
|
514
|
+
},
|
|
515
|
+
},
|
|
516
|
+
},
|
|
517
|
+
context: {
|
|
518
|
+
api: { request },
|
|
519
|
+
routeRepository,
|
|
520
|
+
t: (value: string) => value,
|
|
521
|
+
},
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
const saves = Promise.all([createModel('tab-1', 'Tab 1').save(), createModel('tab-2', 'Tab 2').save()]);
|
|
525
|
+
|
|
526
|
+
await vi.waitFor(() => expect(request).toHaveBeenCalledTimes(2));
|
|
527
|
+
await vi.waitFor(() => expect(refreshAccessible).toHaveBeenCalled());
|
|
528
|
+
await Promise.resolve();
|
|
529
|
+
await Promise.resolve();
|
|
530
|
+
const refreshCallsBeforeRelease = refreshAccessible.mock.calls.length;
|
|
531
|
+
|
|
532
|
+
releaseFirstRefresh();
|
|
533
|
+
await saves;
|
|
534
|
+
|
|
535
|
+
expect(refreshCallsBeforeRelease).toBe(1);
|
|
536
|
+
expect(refreshAccessible).toHaveBeenCalledTimes(2);
|
|
537
|
+
expect(maxActiveRefreshes).toBe(1);
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
it('should keep a successful route save when refreshing the route cache fails', async () => {
|
|
541
|
+
const { RootPageTabModel } = await import('../PageTabModel');
|
|
542
|
+
const refreshError = new Error('refresh failed');
|
|
543
|
+
const request = vi
|
|
544
|
+
.fn()
|
|
545
|
+
.mockResolvedValueOnce({
|
|
546
|
+
data: {
|
|
547
|
+
data: {
|
|
548
|
+
id: 123,
|
|
549
|
+
schemaUid: 'tab-1',
|
|
550
|
+
},
|
|
551
|
+
},
|
|
552
|
+
})
|
|
553
|
+
.mockResolvedValueOnce({});
|
|
554
|
+
const refreshAccessible = vi.fn().mockRejectedValueOnce(refreshError).mockResolvedValueOnce([]);
|
|
555
|
+
const warn = vi.fn();
|
|
556
|
+
const model = createRootPageTabModel(RootPageTabModel, {
|
|
557
|
+
props: {
|
|
558
|
+
route: {
|
|
559
|
+
parentId: 999,
|
|
560
|
+
schemaUid: 'tab-1',
|
|
561
|
+
tabSchemaName: 'tab-name-1',
|
|
562
|
+
type: 'tabs',
|
|
563
|
+
},
|
|
564
|
+
},
|
|
565
|
+
stepParams: {
|
|
566
|
+
pageTabSettings: {
|
|
567
|
+
tab: {
|
|
568
|
+
title: 'Tab title',
|
|
569
|
+
},
|
|
570
|
+
},
|
|
571
|
+
},
|
|
572
|
+
context: {
|
|
573
|
+
api: { request },
|
|
574
|
+
routeRepository: {
|
|
575
|
+
refreshAccessible,
|
|
576
|
+
},
|
|
577
|
+
logger: { warn },
|
|
578
|
+
t: (value: string) => value,
|
|
579
|
+
},
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
await expect(model.save()).resolves.toBeUndefined();
|
|
583
|
+
|
|
584
|
+
expect(model.props.route.id).toBe(123);
|
|
585
|
+
expect(warn).toHaveBeenCalledWith(
|
|
586
|
+
{ err: refreshError },
|
|
587
|
+
'[client-v2] Failed to refresh desktop routes after saving a page tab',
|
|
588
|
+
);
|
|
589
|
+
|
|
590
|
+
await expect(model.save()).resolves.toBeUndefined();
|
|
591
|
+
expect(refreshAccessible).toHaveBeenCalledTimes(2);
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
it('should still reject when writing the tab route fails', async () => {
|
|
595
|
+
const { RootPageTabModel } = await import('../PageTabModel');
|
|
596
|
+
const writeError = new Error('write failed');
|
|
597
|
+
const request = vi.fn().mockRejectedValue(writeError);
|
|
598
|
+
const refreshAccessible = vi.fn().mockResolvedValue([]);
|
|
599
|
+
const warn = vi.fn();
|
|
600
|
+
const model = createRootPageTabModel(RootPageTabModel, {
|
|
601
|
+
props: {
|
|
602
|
+
route: {
|
|
603
|
+
parentId: 999,
|
|
604
|
+
schemaUid: 'tab-1',
|
|
605
|
+
tabSchemaName: 'tab-name-1',
|
|
606
|
+
type: 'tabs',
|
|
607
|
+
},
|
|
608
|
+
},
|
|
609
|
+
stepParams: {
|
|
610
|
+
pageTabSettings: {
|
|
611
|
+
tab: {
|
|
612
|
+
title: 'Tab title',
|
|
613
|
+
},
|
|
614
|
+
},
|
|
615
|
+
},
|
|
616
|
+
context: {
|
|
617
|
+
api: { request },
|
|
618
|
+
routeRepository: {
|
|
619
|
+
refreshAccessible,
|
|
620
|
+
},
|
|
621
|
+
logger: { warn },
|
|
622
|
+
t: (value: string) => value,
|
|
623
|
+
},
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
await expect(model.save()).rejects.toBe(writeError);
|
|
627
|
+
expect(refreshAccessible).not.toHaveBeenCalled();
|
|
628
|
+
expect(warn).not.toHaveBeenCalled();
|
|
629
|
+
expect(model.props.route.id).toBeUndefined();
|
|
630
|
+
});
|
|
631
|
+
|
|
180
632
|
it('should sync persisted route id after save', async () => {
|
|
181
633
|
const { RootPageTabModel } = await import('../PageTabModel');
|
|
182
634
|
const request = vi.fn().mockResolvedValue({
|
|
@@ -188,16 +640,24 @@ describe('PageTabModel', () => {
|
|
|
188
640
|
},
|
|
189
641
|
},
|
|
190
642
|
});
|
|
191
|
-
const model =
|
|
643
|
+
const model = createRootPageTabModel(RootPageTabModel, {
|
|
192
644
|
props: {
|
|
193
645
|
route: {
|
|
646
|
+
parentId: 999,
|
|
194
647
|
schemaUid: 'tab-1',
|
|
648
|
+
tabSchemaName: 'tab-name-1',
|
|
649
|
+
type: 'tabs',
|
|
650
|
+
params: [],
|
|
651
|
+
hideInMenu: false,
|
|
652
|
+
enableTabs: false,
|
|
195
653
|
},
|
|
196
654
|
},
|
|
197
655
|
stepParams: {
|
|
198
656
|
pageTabSettings: {
|
|
199
657
|
tab: {
|
|
200
658
|
title: 'Tab title',
|
|
659
|
+
icon: 'TableOutlined',
|
|
660
|
+
documentTitle: 'Tab document title',
|
|
201
661
|
},
|
|
202
662
|
},
|
|
203
663
|
},
|
|
@@ -205,10 +665,33 @@ describe('PageTabModel', () => {
|
|
|
205
665
|
api: { request },
|
|
206
666
|
t: (value: string) => value,
|
|
207
667
|
},
|
|
208
|
-
}
|
|
668
|
+
});
|
|
209
669
|
|
|
210
670
|
await model.save();
|
|
211
671
|
|
|
672
|
+
const call = request.mock.calls[0][0];
|
|
673
|
+
expect(call).toEqual({
|
|
674
|
+
method: 'post',
|
|
675
|
+
url: 'desktopRoutes:updateOrCreate',
|
|
676
|
+
params: {
|
|
677
|
+
filterKeys: ['schemaUid'],
|
|
678
|
+
},
|
|
679
|
+
data: {
|
|
680
|
+
parentId: 999,
|
|
681
|
+
schemaUid: 'tab-1',
|
|
682
|
+
tabSchemaName: 'tab-name-1',
|
|
683
|
+
type: 'tabs',
|
|
684
|
+
params: [],
|
|
685
|
+
hideInMenu: false,
|
|
686
|
+
enableTabs: false,
|
|
687
|
+
title: 'Tab title',
|
|
688
|
+
icon: 'TableOutlined',
|
|
689
|
+
options: {
|
|
690
|
+
flowRegistry: {},
|
|
691
|
+
documentTitle: 'Tab document title',
|
|
692
|
+
},
|
|
693
|
+
},
|
|
694
|
+
});
|
|
212
695
|
expect(model.props.route.id).toBe(123);
|
|
213
696
|
expect(model.props.route.sort).toBe(9);
|
|
214
697
|
});
|
|
@@ -228,7 +711,7 @@ describe('PageTabModel', () => {
|
|
|
228
711
|
],
|
|
229
712
|
},
|
|
230
713
|
});
|
|
231
|
-
const model =
|
|
714
|
+
const model = createRootPageTabModel(RootPageTabModel, {
|
|
232
715
|
props: {
|
|
233
716
|
route: {
|
|
234
717
|
schemaUid: 'tab-1',
|
|
@@ -248,7 +731,7 @@ describe('PageTabModel', () => {
|
|
|
248
731
|
api: { request },
|
|
249
732
|
t: (value: string) => value,
|
|
250
733
|
},
|
|
251
|
-
}
|
|
734
|
+
});
|
|
252
735
|
|
|
253
736
|
await model.save();
|
|
254
737
|
|
|
@@ -495,7 +978,7 @@ describe('PageTabModel', () => {
|
|
|
495
978
|
await model.saveStepParams();
|
|
496
979
|
|
|
497
980
|
expect(request).toHaveBeenCalledTimes(1);
|
|
498
|
-
expect(request.mock.calls[0][0].url).toBe('desktopRoutes:
|
|
981
|
+
expect(request.mock.calls[0][0].url).toBe('desktopRoutes:update?filter[id]=10');
|
|
499
982
|
expect(updateRoute).not.toHaveBeenCalled();
|
|
500
983
|
});
|
|
501
984
|
|
|
@@ -522,15 +1005,22 @@ describe('PageTabModel', () => {
|
|
|
522
1005
|
},
|
|
523
1006
|
};
|
|
524
1007
|
const request = vi.fn(async ({ url }: { url: string }) => {
|
|
525
|
-
if (url === 'desktopRoutes:updateOrCreate') {
|
|
526
|
-
return { data: { data: { id: 10, schemaUid: 'tab-1' } } };
|
|
527
|
-
}
|
|
528
1008
|
if (url === 'flowModels:findOne') {
|
|
529
1009
|
return { data: { data: latestAnchor } };
|
|
530
1010
|
}
|
|
531
1011
|
return { data: { data: {} } };
|
|
532
1012
|
});
|
|
533
1013
|
const updateRoute = vi.fn().mockResolvedValue(undefined);
|
|
1014
|
+
const currentRoute = {
|
|
1015
|
+
id: 10,
|
|
1016
|
+
schemaUid: 'tab-1',
|
|
1017
|
+
options: {
|
|
1018
|
+
badge: 'new',
|
|
1019
|
+
pluginOption: { keep: true },
|
|
1020
|
+
serverHookState: true,
|
|
1021
|
+
flowRegistry: { routeFlow: { steps: {} } },
|
|
1022
|
+
},
|
|
1023
|
+
};
|
|
534
1024
|
const linkageRules = { value: [{ key: 'new-rule' }] };
|
|
535
1025
|
const model = new RootPageTabModel({
|
|
536
1026
|
uid: 'tab-1',
|
|
@@ -557,7 +1047,11 @@ describe('PageTabModel', () => {
|
|
|
557
1047
|
},
|
|
558
1048
|
context: {
|
|
559
1049
|
api: { request },
|
|
560
|
-
routeRepository: {
|
|
1050
|
+
routeRepository: {
|
|
1051
|
+
getRouteBySchemaUid: vi.fn(() => currentRoute),
|
|
1052
|
+
refreshAccessible: vi.fn().mockResolvedValue([currentRoute]),
|
|
1053
|
+
updateRoute,
|
|
1054
|
+
},
|
|
561
1055
|
t: (value: string) => value,
|
|
562
1056
|
},
|
|
563
1057
|
} as any);
|
|
@@ -565,10 +1059,11 @@ describe('PageTabModel', () => {
|
|
|
565
1059
|
await model.saveStepParams();
|
|
566
1060
|
|
|
567
1061
|
const urls = request.mock.calls.map(([config]) => config.url);
|
|
568
|
-
expect(urls).toEqual(['desktopRoutes:
|
|
1062
|
+
expect(urls).toEqual(['desktopRoutes:update?filter[id]=10', 'flowModels:findOne', 'flowModels:save']);
|
|
569
1063
|
expect(request.mock.calls[0][0].data.options).toMatchObject({
|
|
570
1064
|
badge: 'new',
|
|
571
1065
|
pluginOption: { keep: true },
|
|
1066
|
+
serverHookState: true,
|
|
572
1067
|
flowRegistry: { routeFlow: { steps: {} } },
|
|
573
1068
|
documentTitle: 'Tab document title',
|
|
574
1069
|
});
|
|
@@ -598,6 +1093,7 @@ describe('PageTabModel', () => {
|
|
|
598
1093
|
options: expect.objectContaining({
|
|
599
1094
|
badge: 'new',
|
|
600
1095
|
pluginOption: { keep: true },
|
|
1096
|
+
serverHookState: true,
|
|
601
1097
|
hasPersistedPageTabFlowModel: true,
|
|
602
1098
|
}),
|
|
603
1099
|
},
|
|
@@ -605,6 +1101,7 @@ describe('PageTabModel', () => {
|
|
|
605
1101
|
);
|
|
606
1102
|
expect(request.mock.invocationCallOrder[2]).toBeLessThan(updateRoute.mock.invocationCallOrder[0]);
|
|
607
1103
|
expect(model.props.route.options.hasPersistedPageTabFlowModel).toBe(true);
|
|
1104
|
+
expect(model.props.route.options.serverHookState).toBe(true);
|
|
608
1105
|
});
|
|
609
1106
|
|
|
610
1107
|
it('should read the latest anchor again before every linkage save', async () => {
|
|
@@ -1,3 +1,12 @@
|
|
|
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
|
+
|
|
1
10
|
import { render, screen } from '@nocobase/test/client';
|
|
2
11
|
import { FlowEngine, FlowEngineProvider } from '@nocobase/flow-engine';
|
|
3
12
|
import { App, ConfigProvider } from 'antd';
|
|
@@ -35,3 +44,43 @@ describe('ActionModel rendering', () => {
|
|
|
35
44
|
expect(screen.getByRole('button', { name: 'Open details' })).toBeInTheDocument();
|
|
36
45
|
});
|
|
37
46
|
});
|
|
47
|
+
|
|
48
|
+
describe('ActionModel disabled state persistence', () => {
|
|
49
|
+
it('drops disabled from persisted props during initialization', () => {
|
|
50
|
+
const engine = new FlowEngine();
|
|
51
|
+
engine.registerModels({ NoIconActionModel });
|
|
52
|
+
const model = engine.createModel<NoIconActionModel>({
|
|
53
|
+
use: 'NoIconActionModel',
|
|
54
|
+
props: { title: 'Open details', disabled: true },
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
expect(model.getProps()).not.toHaveProperty('disabled');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('keeps runtime disabled state without serializing it', () => {
|
|
61
|
+
const engine = new FlowEngine();
|
|
62
|
+
engine.registerModels({ NoIconActionModel });
|
|
63
|
+
const model = engine.createModel<NoIconActionModel>({
|
|
64
|
+
use: 'NoIconActionModel',
|
|
65
|
+
props: { title: 'Open details' },
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
model.setProps('disabled', true);
|
|
69
|
+
|
|
70
|
+
expect(model.getProps().disabled).toBe(true);
|
|
71
|
+
expect(model.serialize().props).toEqual({ title: 'Open details' });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('does not serialize fork-local disabled state', () => {
|
|
75
|
+
const engine = new FlowEngine();
|
|
76
|
+
engine.registerModels({ NoIconActionModel });
|
|
77
|
+
const model = engine.createModel<NoIconActionModel>({
|
|
78
|
+
use: 'NoIconActionModel',
|
|
79
|
+
props: { title: 'Open details' },
|
|
80
|
+
});
|
|
81
|
+
const fork = model.createFork({ disabled: true });
|
|
82
|
+
|
|
83
|
+
expect(fork.getProps().disabled).toBe(true);
|
|
84
|
+
expect(fork.serialize().props).toEqual({ title: 'Open details' });
|
|
85
|
+
});
|
|
86
|
+
});
|