@nocobase/client-v2 2.2.0-beta.1 → 2.2.0-beta.3
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/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +5 -0
- package/es/flow/utils/dataScopeRowSnapshot.d.ts +59 -0
- package/es/flow/utils/formValueDeps.d.ts +32 -0
- package/es/index.mjs +68 -68
- package/lib/index.js +82 -82
- package/package.json +7 -7
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +199 -0
- package/src/components/AppComponents.tsx +19 -3
- package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +1022 -0
- package/src/flow/actions/__tests__/subFormFieldLinkageRules.inputArgs.test.ts +2 -2
- package/src/flow/actions/linkageRules.tsx +1 -1
- package/src/flow/actions/linkageRulesFormValueRefresh.ts +22 -130
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +117 -18
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableColumnModel.rowRecord.test.ts +185 -0
- package/src/flow/models/fields/InputFieldModel.tsx +48 -2
- package/src/flow/models/fields/TextareaFieldModel.tsx +49 -3
- package/src/flow/models/fields/__tests__/InputFieldModel.test.tsx +100 -1
- package/src/flow/models/fields/__tests__/TextareaFieldModel.test.tsx +100 -0
- package/src/flow/utils/dataScopeFormValueClear.ts +218 -81
- package/src/flow/utils/dataScopeRowSnapshot.ts +616 -0
- package/src/flow/utils/formValueDeps.ts +170 -0
- package/src/nocobase-buildin-plugin/index.tsx +44 -12
- package/src/settings-center/AdminSettingsLayout.tsx +7 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/client-v2",
|
|
3
|
-
"version": "2.2.0-beta.
|
|
3
|
+
"version": "2.2.0-beta.3",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"module": "es/index.mjs",
|
|
@@ -27,11 +27,11 @@
|
|
|
27
27
|
"@formily/antd-v5": "1.2.3",
|
|
28
28
|
"@formily/react": "^2.2.27",
|
|
29
29
|
"@formily/shared": "^2.2.27",
|
|
30
|
-
"@nocobase/evaluators": "2.2.0-beta.
|
|
31
|
-
"@nocobase/flow-engine": "2.2.0-beta.
|
|
32
|
-
"@nocobase/sdk": "2.2.0-beta.
|
|
33
|
-
"@nocobase/shared": "2.2.0-beta.
|
|
34
|
-
"@nocobase/utils": "2.2.0-beta.
|
|
30
|
+
"@nocobase/evaluators": "2.2.0-beta.3",
|
|
31
|
+
"@nocobase/flow-engine": "2.2.0-beta.3",
|
|
32
|
+
"@nocobase/sdk": "2.2.0-beta.3",
|
|
33
|
+
"@nocobase/shared": "2.2.0-beta.3",
|
|
34
|
+
"@nocobase/utils": "2.2.0-beta.3",
|
|
35
35
|
"ahooks": "^3.7.2",
|
|
36
36
|
"antd": "5.24.2",
|
|
37
37
|
"antd-style": "3.7.1",
|
|
@@ -46,5 +46,5 @@
|
|
|
46
46
|
"react-i18next": "^11.15.1",
|
|
47
47
|
"react-router-dom": "^6.30.1"
|
|
48
48
|
},
|
|
49
|
-
"gitHead": "
|
|
49
|
+
"gitHead": "7b16bb2cfd427c110c6671252138cd85155723c5"
|
|
50
50
|
}
|
|
@@ -12,6 +12,10 @@ import { act, render, screen, waitFor } from '@testing-library/react';
|
|
|
12
12
|
import React from 'react';
|
|
13
13
|
import { NocoBaseBuildInPlugin } from '../nocobase-buildin-plugin';
|
|
14
14
|
|
|
15
|
+
const TestingAppSpin = () => <div data-testid="app-spin">app spin</div>;
|
|
16
|
+
|
|
17
|
+
const TestingAppError = ({ error }: { error: Error }) => <div role="alert">{error.message}</div>;
|
|
18
|
+
|
|
15
19
|
class SkippedPublicRoutePlugin extends Plugin {
|
|
16
20
|
async load() {
|
|
17
21
|
this.router.add('public', {
|
|
@@ -22,6 +26,31 @@ class SkippedPublicRoutePlugin extends Plugin {
|
|
|
22
26
|
}
|
|
23
27
|
}
|
|
24
28
|
|
|
29
|
+
class DummySigninRoutePlugin extends Plugin {
|
|
30
|
+
async load() {
|
|
31
|
+
this.router.add('signin-test', {
|
|
32
|
+
path: '/signin',
|
|
33
|
+
skipAuthCheck: true,
|
|
34
|
+
Component: () => <div>signin page</div>,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
class AuthBootstrapRoutePlugin extends Plugin {
|
|
40
|
+
async load() {
|
|
41
|
+
this.router.add('secure', {
|
|
42
|
+
path: '/secure',
|
|
43
|
+
authCheck: true,
|
|
44
|
+
Component: () => <div>secure page</div>,
|
|
45
|
+
});
|
|
46
|
+
this.router.add('guest', {
|
|
47
|
+
path: '/guest',
|
|
48
|
+
authCheck: false,
|
|
49
|
+
Component: () => <div>guest page</div>,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
25
54
|
describe('nocobase buildin plugin auth redirect', () => {
|
|
26
55
|
const originalLocation = globalThis.window.location;
|
|
27
56
|
|
|
@@ -155,6 +184,176 @@ describe('nocobase buildin plugin auth redirect', () => {
|
|
|
155
184
|
});
|
|
156
185
|
});
|
|
157
186
|
|
|
187
|
+
it('should defer data source bootstrap until auth-required route is authenticated after signin redirect', async () => {
|
|
188
|
+
const app = createMockClient({
|
|
189
|
+
publicPath: '/v2/',
|
|
190
|
+
plugins: [NocoBaseBuildInPlugin as any, DummySigninRoutePlugin as any, AuthBootstrapRoutePlugin as any],
|
|
191
|
+
components: { AppSpin: TestingAppSpin },
|
|
192
|
+
router: { type: 'memory', initialEntries: ['/v2/secure'] },
|
|
193
|
+
});
|
|
194
|
+
app.apiMock.onGet('app:getLang').reply(200, {
|
|
195
|
+
data: { lang: 'en-US', resources: { client: {} }, cron: {} },
|
|
196
|
+
});
|
|
197
|
+
const events: string[] = [];
|
|
198
|
+
app.apiMock.onGet('/auth:check').replyOnce(() => {
|
|
199
|
+
events.push('auth:first');
|
|
200
|
+
return [200, { data: {} }];
|
|
201
|
+
});
|
|
202
|
+
app.apiMock.onGet('/auth:check').reply(() => {
|
|
203
|
+
events.push('auth:second');
|
|
204
|
+
return [200, { data: { id: 1 } }];
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const ensureLoaded = vi.spyOn(app.dataSourceManager, 'ensureLoaded').mockImplementation(() => {
|
|
208
|
+
if (!app.apiClient.auth.token) {
|
|
209
|
+
const pending = new Promise<void>(() => undefined);
|
|
210
|
+
app.dataSourceManager.loadingPromise = pending;
|
|
211
|
+
return pending;
|
|
212
|
+
}
|
|
213
|
+
events.push('bootstrap');
|
|
214
|
+
expect(events).toContain('auth:second');
|
|
215
|
+
return Promise.resolve();
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const Root = app.getRootComponent();
|
|
219
|
+
render(<Root />);
|
|
220
|
+
|
|
221
|
+
await waitFor(() => {
|
|
222
|
+
expect(app.router.router.state.location.pathname).toBe('/v2/signin');
|
|
223
|
+
expect(app.router.router.state.location.search).toBe('?redirect=%2Fv2%2Fsecure');
|
|
224
|
+
});
|
|
225
|
+
expect(await screen.findByText('signin page')).toBeInTheDocument();
|
|
226
|
+
expect(screen.queryByTestId('app-spin')).not.toBeInTheDocument();
|
|
227
|
+
expect(ensureLoaded).not.toHaveBeenCalled();
|
|
228
|
+
expect(app.dataSourceManager.loadingPromise).toBeNull();
|
|
229
|
+
expect(events).toEqual(['auth:first']);
|
|
230
|
+
|
|
231
|
+
act(() => {
|
|
232
|
+
app.apiClient.auth.setToken('test-token');
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
await act(async () => {
|
|
236
|
+
await app.router.router.navigate('/secure');
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
expect(await screen.findByText('secure page')).toBeInTheDocument();
|
|
240
|
+
await waitFor(() => {
|
|
241
|
+
expect(document.querySelector('.ant-spin-spinning')).not.toBeInTheDocument();
|
|
242
|
+
});
|
|
243
|
+
expect(ensureLoaded).toHaveBeenCalledTimes(1);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('should redirect on auth-required /auth:check 401 without bootstrapping data sources', async () => {
|
|
247
|
+
const app = createMockClient({
|
|
248
|
+
publicPath: '/v2/',
|
|
249
|
+
plugins: [NocoBaseBuildInPlugin as any, DummySigninRoutePlugin as any, AuthBootstrapRoutePlugin as any],
|
|
250
|
+
components: { AppSpin: TestingAppSpin },
|
|
251
|
+
router: { type: 'memory', initialEntries: ['/v2/secure'] },
|
|
252
|
+
});
|
|
253
|
+
app.apiMock.onGet('app:getLang').reply(200, {
|
|
254
|
+
data: { lang: 'en-US', resources: { client: {} }, cron: {} },
|
|
255
|
+
});
|
|
256
|
+
app.apiMock.onGet('/auth:check').reply(401, { errors: [{ code: 'EMPTY_TOKEN' }] });
|
|
257
|
+
const ensureLoaded = vi.spyOn(app.dataSourceManager, 'ensureLoaded').mockImplementation(() => {
|
|
258
|
+
const pending = new Promise<void>(() => undefined);
|
|
259
|
+
app.dataSourceManager.loadingPromise = pending;
|
|
260
|
+
return pending;
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const Root = app.getRootComponent();
|
|
264
|
+
render(<Root />);
|
|
265
|
+
|
|
266
|
+
await waitFor(() => {
|
|
267
|
+
expect(app.router.router.state.location.pathname).toBe('/v2/signin');
|
|
268
|
+
expect(app.router.router.state.location.search).toBe('?redirect=%2Fv2%2Fsecure');
|
|
269
|
+
});
|
|
270
|
+
expect(await screen.findByText('signin page')).toBeInTheDocument();
|
|
271
|
+
expect(screen.queryByTestId('app-spin')).not.toBeInTheDocument();
|
|
272
|
+
expect(ensureLoaded).not.toHaveBeenCalled();
|
|
273
|
+
expect(app.dataSourceManager.loadingPromise).toBeNull();
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('should surface non-auth /auth:check errors instead of leaving auth-required route spinning', async () => {
|
|
277
|
+
const app = createMockClient({
|
|
278
|
+
publicPath: '/v2/',
|
|
279
|
+
plugins: [NocoBaseBuildInPlugin as any, AuthBootstrapRoutePlugin as any],
|
|
280
|
+
components: { AppSpin: TestingAppSpin, AppError: TestingAppError },
|
|
281
|
+
router: { type: 'memory', initialEntries: ['/v2/secure'] },
|
|
282
|
+
});
|
|
283
|
+
app.apiMock.onGet('app:getLang').reply(200, {
|
|
284
|
+
data: { lang: 'en-US', resources: { client: {} }, cron: {} },
|
|
285
|
+
});
|
|
286
|
+
app.apiMock.onGet('/auth:check').reply(500, { errors: [{ message: 'auth check failed' }] });
|
|
287
|
+
const ensureLoaded = vi.spyOn(app.dataSourceManager, 'ensureLoaded').mockResolvedValue(undefined);
|
|
288
|
+
|
|
289
|
+
const Root = app.getRootComponent();
|
|
290
|
+
render(<Root />);
|
|
291
|
+
|
|
292
|
+
expect(await screen.findByRole('alert')).toHaveTextContent('Request failed with status code 500');
|
|
293
|
+
expect(screen.queryByTestId('app-spin')).not.toBeInTheDocument();
|
|
294
|
+
expect(ensureLoaded).not.toHaveBeenCalled();
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it.each(['/v2/signin', '/v2/public'])('should not bootstrap data sources on skipped auth route: %s', async (path) => {
|
|
298
|
+
const app = createMockClient({
|
|
299
|
+
publicPath: '/v2/',
|
|
300
|
+
plugins: [NocoBaseBuildInPlugin as any, DummySigninRoutePlugin as any, SkippedPublicRoutePlugin as any],
|
|
301
|
+
components: { AppSpin: TestingAppSpin },
|
|
302
|
+
router: { type: 'memory', initialEntries: [path] },
|
|
303
|
+
});
|
|
304
|
+
app.apiMock.onGet('app:getLang').reply(200, {
|
|
305
|
+
data: { lang: 'en-US', resources: { client: {} }, cron: {} },
|
|
306
|
+
});
|
|
307
|
+
const ensureLoaded = vi.spyOn(app.dataSourceManager, 'ensureLoaded').mockResolvedValue(undefined);
|
|
308
|
+
|
|
309
|
+
const Root = app.getRootComponent();
|
|
310
|
+
render(<Root />);
|
|
311
|
+
|
|
312
|
+
expect(await screen.findByText(path === '/v2/signin' ? 'signin page' : 'public page')).toBeInTheDocument();
|
|
313
|
+
expect(screen.queryByTestId('app-spin')).not.toBeInTheDocument();
|
|
314
|
+
expect(ensureLoaded).not.toHaveBeenCalled();
|
|
315
|
+
expect(app.apiMock.history.get.filter((request) => request.url === '/auth:check')).toHaveLength(0);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('should bootstrap data sources for authenticated auth-required route access', async () => {
|
|
319
|
+
const app = createMockClient({
|
|
320
|
+
publicPath: '/v2/',
|
|
321
|
+
plugins: [NocoBaseBuildInPlugin as any, AuthBootstrapRoutePlugin as any],
|
|
322
|
+
router: { type: 'memory', initialEntries: ['/v2/secure'] },
|
|
323
|
+
});
|
|
324
|
+
app.apiClient.auth.setToken('test-token');
|
|
325
|
+
app.apiMock.onGet('app:getLang').reply(200, {
|
|
326
|
+
data: { lang: 'en-US', resources: { client: {} }, cron: {} },
|
|
327
|
+
});
|
|
328
|
+
app.apiMock.onGet('/auth:check').reply(200, { data: { id: 1 } });
|
|
329
|
+
const ensureLoaded = vi.spyOn(app.dataSourceManager, 'ensureLoaded').mockResolvedValue(undefined);
|
|
330
|
+
|
|
331
|
+
const Root = app.getRootComponent();
|
|
332
|
+
render(<Root />);
|
|
333
|
+
|
|
334
|
+
expect(await screen.findByText('secure page')).toBeInTheDocument();
|
|
335
|
+
expect(ensureLoaded).toHaveBeenCalledTimes(1);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('should not auth-check or bootstrap authCheck false routes', async () => {
|
|
339
|
+
const app = createMockClient({
|
|
340
|
+
publicPath: '/v2/',
|
|
341
|
+
plugins: [NocoBaseBuildInPlugin as any, AuthBootstrapRoutePlugin as any],
|
|
342
|
+
router: { type: 'memory', initialEntries: ['/v2/guest'] },
|
|
343
|
+
});
|
|
344
|
+
app.apiMock.onGet('app:getLang').reply(200, {
|
|
345
|
+
data: { lang: 'en-US', resources: { client: {} }, cron: {} },
|
|
346
|
+
});
|
|
347
|
+
const ensureLoaded = vi.spyOn(app.dataSourceManager, 'ensureLoaded').mockResolvedValue(undefined);
|
|
348
|
+
|
|
349
|
+
const Root = app.getRootComponent();
|
|
350
|
+
render(<Root />);
|
|
351
|
+
|
|
352
|
+
expect(await screen.findByText('guest page')).toBeInTheDocument();
|
|
353
|
+
expect(ensureLoaded).not.toHaveBeenCalled();
|
|
354
|
+
expect(app.apiMock.history.get.filter((request) => request.url === '/auth:check')).toHaveLength(0);
|
|
355
|
+
});
|
|
356
|
+
|
|
158
357
|
it('should render v2 admin root without redirecting away', async () => {
|
|
159
358
|
const app = createMockClient({
|
|
160
359
|
publicPath: '/v2/',
|
|
@@ -21,7 +21,13 @@ import type { Application } from '../Application';
|
|
|
21
21
|
interface AppErrorPayload {
|
|
22
22
|
code?: string;
|
|
23
23
|
message?: string;
|
|
24
|
-
command?: {
|
|
24
|
+
command?: {
|
|
25
|
+
name: string;
|
|
26
|
+
components?: {
|
|
27
|
+
maintaining?: string;
|
|
28
|
+
maintainingDialog?: string;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
25
31
|
[key: string]: any;
|
|
26
32
|
}
|
|
27
33
|
|
|
@@ -188,7 +194,12 @@ export const AppError: FC<{ error: Error & { title?: string }; app: Application
|
|
|
188
194
|
);
|
|
189
195
|
|
|
190
196
|
export const AppMaintaining: FC<{ app: Application; error: Error }> = observer(
|
|
191
|
-
({ app }) => {
|
|
197
|
+
({ app, error }) => {
|
|
198
|
+
const component = (error as AppErrorPayload | undefined)?.command?.components?.maintaining;
|
|
199
|
+
if (component) {
|
|
200
|
+
return app.renderComponent(component, { app, error });
|
|
201
|
+
}
|
|
202
|
+
|
|
192
203
|
const { icon, status, title, subTitle } = getProps(app);
|
|
193
204
|
return (
|
|
194
205
|
<div>
|
|
@@ -211,7 +222,12 @@ export const AppMaintaining: FC<{ app: Application; error: Error }> = observer(
|
|
|
211
222
|
);
|
|
212
223
|
|
|
213
224
|
export const AppMaintainingDialog: FC<{ app: Application; error: Error }> = observer(
|
|
214
|
-
({ app }) => {
|
|
225
|
+
({ app, error }) => {
|
|
226
|
+
const component = (error as AppErrorPayload | undefined)?.command?.components?.maintainingDialog;
|
|
227
|
+
if (component) {
|
|
228
|
+
return app.renderComponent(component, { app, error });
|
|
229
|
+
}
|
|
230
|
+
|
|
215
231
|
const { icon, status, title, subTitle } = getProps(app);
|
|
216
232
|
return (
|
|
217
233
|
<Modal open={true} footer={null} closable={false}>
|