@nocobase/client-v2 2.0.0-alpha.20
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/LICENSE.txt +172 -0
- package/lib/Application.d.ts +124 -0
- package/lib/Application.js +489 -0
- package/lib/MockApplication.d.ts +16 -0
- package/lib/MockApplication.js +96 -0
- package/lib/Plugin.d.ts +33 -0
- package/lib/Plugin.js +89 -0
- package/lib/PluginManager.d.ts +46 -0
- package/lib/PluginManager.js +114 -0
- package/lib/PluginSettingsManager.d.ts +67 -0
- package/lib/PluginSettingsManager.js +148 -0
- package/lib/RouterManager.d.ts +61 -0
- package/lib/RouterManager.js +198 -0
- package/lib/WebSocketClient.d.ts +45 -0
- package/lib/WebSocketClient.js +217 -0
- package/lib/components/BlankComponent.d.ts +12 -0
- package/lib/components/BlankComponent.js +48 -0
- package/lib/components/MainComponent.d.ts +10 -0
- package/lib/components/MainComponent.js +54 -0
- package/lib/components/RouterBridge.d.ts +13 -0
- package/lib/components/RouterBridge.js +66 -0
- package/lib/components/RouterContextCleaner.d.ts +12 -0
- package/lib/components/RouterContextCleaner.js +61 -0
- package/lib/components/index.d.ts +10 -0
- package/lib/components/index.js +32 -0
- package/lib/context.d.ts +11 -0
- package/lib/context.js +38 -0
- package/lib/hooks/index.d.ts +11 -0
- package/lib/hooks/index.js +34 -0
- package/lib/hooks/useApp.d.ts +10 -0
- package/lib/hooks/useApp.js +41 -0
- package/lib/hooks/usePlugin.d.ts +11 -0
- package/lib/hooks/usePlugin.js +42 -0
- package/lib/hooks/useRouter.d.ts +9 -0
- package/lib/hooks/useRouter.js +41 -0
- package/lib/index.d.ts +14 -0
- package/lib/index.js +40 -0
- package/lib/utils/index.d.ts +11 -0
- package/lib/utils/index.js +79 -0
- package/lib/utils/remotePlugins.d.ts +44 -0
- package/lib/utils/remotePlugins.js +131 -0
- package/lib/utils/requirejs.d.ts +18 -0
- package/lib/utils/requirejs.js +1361 -0
- package/lib/utils/types.d.ts +330 -0
- package/lib/utils/types.js +28 -0
- package/package.json +16 -0
- package/src/Application.tsx +539 -0
- package/src/MockApplication.tsx +53 -0
- package/src/Plugin.ts +78 -0
- package/src/PluginManager.ts +114 -0
- package/src/PluginSettingsManager.ts +182 -0
- package/src/RouterManager.tsx +239 -0
- package/src/WebSocketClient.ts +220 -0
- package/src/__tests__/app.test.tsx +141 -0
- package/src/components/BlankComponent.tsx +12 -0
- package/src/components/MainComponent.tsx +20 -0
- package/src/components/RouterBridge.tsx +38 -0
- package/src/components/RouterContextCleaner.tsx +26 -0
- package/src/components/index.ts +11 -0
- package/src/context.ts +14 -0
- package/src/hooks/index.ts +12 -0
- package/src/hooks/useApp.ts +16 -0
- package/src/hooks/usePlugin.ts +17 -0
- package/src/hooks/useRouter.ts +15 -0
- package/src/index.ts +15 -0
- package/src/utils/index.tsx +48 -0
- package/src/utils/remotePlugins.ts +140 -0
- package/src/utils/requirejs.ts +2164 -0
- package/src/utils/types.ts +375 -0
- package/tsconfig.json +7 -0
|
@@ -0,0 +1,539 @@
|
|
|
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 { define, observable } from '@formily/reactive';
|
|
11
|
+
import {
|
|
12
|
+
FlowEngine,
|
|
13
|
+
FlowEngineContext,
|
|
14
|
+
FlowEngineGlobalsContextProvider,
|
|
15
|
+
FlowEngineProvider,
|
|
16
|
+
FlowModel,
|
|
17
|
+
FlowModelRenderer,
|
|
18
|
+
} from '@nocobase/flow-engine';
|
|
19
|
+
import { APIClient, type APIClientOptions, getSubAppName } from '@nocobase/sdk';
|
|
20
|
+
import { createInstance, type i18n as i18next } from 'i18next';
|
|
21
|
+
import _ from 'lodash';
|
|
22
|
+
import React, { ComponentType, FC, ReactElement, ReactNode } from 'react';
|
|
23
|
+
import { createRoot } from 'react-dom/client';
|
|
24
|
+
import { I18nextProvider } from 'react-i18next';
|
|
25
|
+
import { Link, NavLink, Navigate } from 'react-router-dom';
|
|
26
|
+
import type { Plugin } from './Plugin';
|
|
27
|
+
import { PluginManager, type PluginType } from './PluginManager';
|
|
28
|
+
import { type PluginSettingOptions, PluginSettingsManager } from './PluginSettingsManager';
|
|
29
|
+
import { type ComponentTypeAndString, RouterManager, type RouterOptions } from './RouterManager';
|
|
30
|
+
import { WebSocketClient, type WebSocketClientOptions } from './WebSocketClient';
|
|
31
|
+
import { BlankComponent } from './components';
|
|
32
|
+
import { compose, normalizeContainer } from './utils';
|
|
33
|
+
import type { RequireJS } from './utils/requirejs';
|
|
34
|
+
import { getRequireJs } from './utils/requirejs';
|
|
35
|
+
|
|
36
|
+
declare global {
|
|
37
|
+
interface Window {
|
|
38
|
+
define: RequireJS['define'];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type DevDynamicImport = (packageName: string) => Promise<{ default: typeof Plugin }>;
|
|
43
|
+
export type ComponentAndProps<T = any> = [ComponentType, T];
|
|
44
|
+
export interface ApplicationOptions {
|
|
45
|
+
name?: string;
|
|
46
|
+
publicPath?: string;
|
|
47
|
+
apiClient?: APIClientOptions;
|
|
48
|
+
ws?: WebSocketClientOptions | boolean;
|
|
49
|
+
i18n?: i18next;
|
|
50
|
+
providers?: (ComponentType | ComponentAndProps)[];
|
|
51
|
+
plugins?: PluginType[];
|
|
52
|
+
components?: Record<string, ComponentType>;
|
|
53
|
+
scopes?: Record<string, any>;
|
|
54
|
+
router?: RouterOptions;
|
|
55
|
+
pluginSettings?: Record<string, PluginSettingOptions>;
|
|
56
|
+
designable?: boolean;
|
|
57
|
+
loadRemotePlugins?: boolean;
|
|
58
|
+
devDynamicImport?: DevDynamicImport;
|
|
59
|
+
disableAcl?: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export class Application {
|
|
63
|
+
public eventBus = new EventTarget();
|
|
64
|
+
public providers: ComponentAndProps[] = [];
|
|
65
|
+
public router: RouterManager;
|
|
66
|
+
public scopes: Record<string, any> = {};
|
|
67
|
+
public i18n: i18next;
|
|
68
|
+
public ws: WebSocketClient;
|
|
69
|
+
public apiClient: APIClient;
|
|
70
|
+
public components: Record<string, ComponentType<any> | any> = {
|
|
71
|
+
AppNotFound: () => <div>Not Found</div>,
|
|
72
|
+
AppError: () => <div>{this.error?.message}</div>,
|
|
73
|
+
AppSpin: () => <div>Loading</div>,
|
|
74
|
+
AppMaintaining: () => <div>Maintaining</div>,
|
|
75
|
+
AppMaintainingDialog: () => <div>Maintaining Dialog</div>,
|
|
76
|
+
};
|
|
77
|
+
public pluginManager: PluginManager;
|
|
78
|
+
public pluginSettingsManager: PluginSettingsManager;
|
|
79
|
+
public devDynamicImport: DevDynamicImport;
|
|
80
|
+
public requirejs: RequireJS;
|
|
81
|
+
public name: string;
|
|
82
|
+
public favicon: string;
|
|
83
|
+
public flowEngine: FlowEngine;
|
|
84
|
+
public context: FlowEngineContext & {
|
|
85
|
+
pluginSettingsRouter: PluginSettingsManager;
|
|
86
|
+
pluginManager: PluginManager;
|
|
87
|
+
};
|
|
88
|
+
maintained = false;
|
|
89
|
+
maintaining = false;
|
|
90
|
+
error = null;
|
|
91
|
+
|
|
92
|
+
model: ApplicationModel;
|
|
93
|
+
|
|
94
|
+
private wsAuthorized = false;
|
|
95
|
+
apps: {
|
|
96
|
+
Component?: ComponentType;
|
|
97
|
+
} = {
|
|
98
|
+
Component: null,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
get pm() {
|
|
102
|
+
return this.pluginManager;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
get disableAcl() {
|
|
106
|
+
return this.options.disableAcl;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
get isWsAuthorized() {
|
|
110
|
+
return this.wsAuthorized;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
updateFavicon(favicon?: string) {
|
|
114
|
+
let faviconLinkElement: HTMLLinkElement = document.querySelector('link[rel="shortcut icon"]');
|
|
115
|
+
|
|
116
|
+
if (favicon) {
|
|
117
|
+
this.favicon = favicon;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!faviconLinkElement) {
|
|
121
|
+
faviconLinkElement = document.createElement('link');
|
|
122
|
+
faviconLinkElement.rel = 'shortcut icon';
|
|
123
|
+
faviconLinkElement.href = this.favicon || '/favicon/favicon.ico';
|
|
124
|
+
document.head.appendChild(faviconLinkElement);
|
|
125
|
+
} else {
|
|
126
|
+
faviconLinkElement.href = this.favicon || '/favicon/favicon.ico';
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
setWsAuthorized(authorized: boolean) {
|
|
131
|
+
this.wsAuthorized = authorized;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
constructor(protected options: ApplicationOptions = {}) {
|
|
135
|
+
this.initRequireJs();
|
|
136
|
+
define(this, {
|
|
137
|
+
maintained: observable.ref,
|
|
138
|
+
maintaining: observable.ref,
|
|
139
|
+
error: observable.ref,
|
|
140
|
+
});
|
|
141
|
+
this.devDynamicImport = options.devDynamicImport;
|
|
142
|
+
this.components = _.merge(this.components, options.components);
|
|
143
|
+
this.apiClient = new APIClient(options.apiClient);
|
|
144
|
+
this.i18n = options.i18n || createInstance();
|
|
145
|
+
this.router = new RouterManager(options.router, this);
|
|
146
|
+
this.pluginManager = new PluginManager(options.plugins, options.loadRemotePlugins, this);
|
|
147
|
+
this.flowEngine = new FlowEngine();
|
|
148
|
+
this.flowEngine.registerModels({ ApplicationModel });
|
|
149
|
+
this.model = this.flowEngine.createModel<ApplicationModel>({
|
|
150
|
+
use: 'ApplicationModel',
|
|
151
|
+
uid: '__app_model__',
|
|
152
|
+
});
|
|
153
|
+
this.context = this.flowEngine.context as any;
|
|
154
|
+
this.context.defineProperty('pluginManager', {
|
|
155
|
+
get: () => this.pluginManager,
|
|
156
|
+
});
|
|
157
|
+
this.context.defineProperty('pluginSettingsRouter', {
|
|
158
|
+
get: () => this.pluginSettingsManager,
|
|
159
|
+
});
|
|
160
|
+
this.addDefaultProviders();
|
|
161
|
+
this.addReactRouterComponents();
|
|
162
|
+
this.addProviders(options.providers || []);
|
|
163
|
+
this.ws = new WebSocketClient(options.ws);
|
|
164
|
+
this.ws.app = this;
|
|
165
|
+
this.pluginSettingsManager = new PluginSettingsManager(options.pluginSettings, this);
|
|
166
|
+
this.addRoutes();
|
|
167
|
+
this.name = this.options.name || getSubAppName(options.publicPath) || 'main';
|
|
168
|
+
this.i18n.on('languageChanged', (lng) => {
|
|
169
|
+
this.apiClient.auth.locale = lng;
|
|
170
|
+
});
|
|
171
|
+
this.initListeners();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private initListeners() {
|
|
175
|
+
this.eventBus.addEventListener('auth:tokenChanged', (event: CustomEvent) => {
|
|
176
|
+
this.setTokenInWebSocket(event.detail);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
this.eventBus.addEventListener('maintaining:end', () => {
|
|
180
|
+
if (this.apiClient.auth.token) {
|
|
181
|
+
this.setTokenInWebSocket({
|
|
182
|
+
token: this.apiClient.auth.token,
|
|
183
|
+
authenticator: this.apiClient.auth.getAuthenticator(),
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
protected setTokenInWebSocket(options: { token: string; authenticator: string }) {
|
|
190
|
+
const { token, authenticator } = options;
|
|
191
|
+
if (this.maintaining) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
this.ws.send(
|
|
196
|
+
JSON.stringify({
|
|
197
|
+
type: 'auth:token',
|
|
198
|
+
payload: {
|
|
199
|
+
token,
|
|
200
|
+
authenticator,
|
|
201
|
+
},
|
|
202
|
+
}),
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
setMaintaining(maintaining: boolean) {
|
|
207
|
+
// if maintaining is the same, do nothing
|
|
208
|
+
if (this.maintaining === maintaining) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
this.maintaining = maintaining;
|
|
213
|
+
if (!maintaining) {
|
|
214
|
+
this.eventBus.dispatchEvent(new Event('maintaining:end'));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private initRequireJs() {
|
|
219
|
+
if (typeof window === 'undefined') {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
// 避免重复初始化 requirejs
|
|
223
|
+
if (window['requirejs']) {
|
|
224
|
+
this.requirejs = window['requirejs'];
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
window['requirejs'] = this.requirejs = getRequireJs();
|
|
228
|
+
window.define = this.requirejs.define;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private addDefaultProviders() {
|
|
232
|
+
this.use(I18nextProvider, { i18n: this.i18n });
|
|
233
|
+
this.flowEngine.context.defineProperty('app', {
|
|
234
|
+
value: this,
|
|
235
|
+
});
|
|
236
|
+
this.flowEngine.context.defineProperty('api', {
|
|
237
|
+
value: this.apiClient,
|
|
238
|
+
});
|
|
239
|
+
this.flowEngine.context.defineProperty('i18n', {
|
|
240
|
+
value: this.i18n,
|
|
241
|
+
});
|
|
242
|
+
this.flowEngine.context.defineProperty('router', {
|
|
243
|
+
get: () => this.router.router,
|
|
244
|
+
cache: false,
|
|
245
|
+
});
|
|
246
|
+
this.flowEngine.context.defineProperty('documentTitle', {
|
|
247
|
+
get: () => document.title,
|
|
248
|
+
});
|
|
249
|
+
this.flowEngine.context.defineProperty('route', {
|
|
250
|
+
get: () => {},
|
|
251
|
+
observable: true,
|
|
252
|
+
});
|
|
253
|
+
this.flowEngine.context.defineProperty('location', {
|
|
254
|
+
get: () => location,
|
|
255
|
+
observable: true,
|
|
256
|
+
});
|
|
257
|
+
this.use(FlowEngineProvider, { engine: this.flowEngine });
|
|
258
|
+
this.use(FlowEngineGlobalsContextProvider);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private addReactRouterComponents() {
|
|
262
|
+
this.addComponents({
|
|
263
|
+
Link,
|
|
264
|
+
Navigate: Navigate as ComponentType,
|
|
265
|
+
NavLink: NavLink as ComponentType,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private addRoutes() {
|
|
270
|
+
this.router.add('not-found', {
|
|
271
|
+
path: '*',
|
|
272
|
+
Component: this.components['AppNotFound'],
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
getOptions() {
|
|
277
|
+
return this.options;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
getName() {
|
|
281
|
+
return getSubAppName(this.getPublicPath()) || null;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
getPublicPath() {
|
|
285
|
+
let publicPath = this.options.publicPath || '/';
|
|
286
|
+
if (!publicPath.endsWith('/')) {
|
|
287
|
+
publicPath += '/';
|
|
288
|
+
}
|
|
289
|
+
return publicPath;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
getApiUrl(pathname = '') {
|
|
293
|
+
let baseURL = this.apiClient.axios['defaults']['baseURL'];
|
|
294
|
+
if (!baseURL.startsWith('http://') && !baseURL.startsWith('https://')) {
|
|
295
|
+
const { protocol, host } = window.location;
|
|
296
|
+
baseURL = `${protocol}//${host}${baseURL}`;
|
|
297
|
+
}
|
|
298
|
+
return baseURL.replace(/\/$/g, '') + '/' + pathname.replace(/^\//g, '');
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
getRouteUrl(pathname: string) {
|
|
302
|
+
return this.getPublicPath() + pathname.replace(/^\//g, '');
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
getHref(pathname: string) {
|
|
306
|
+
const name = this.name;
|
|
307
|
+
if (name && name !== 'main') {
|
|
308
|
+
return this.getPublicPath() + 'apps/' + name + '/' + pathname.replace(/^\//g, '');
|
|
309
|
+
}
|
|
310
|
+
return this.getPublicPath() + pathname.replace(/^\//g, '');
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* @internal
|
|
315
|
+
*/
|
|
316
|
+
getComposeProviders() {
|
|
317
|
+
const Providers = compose(...this.providers)(BlankComponent);
|
|
318
|
+
Providers.displayName = 'Providers';
|
|
319
|
+
return Providers;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
use<T = any>(component: ComponentType, props?: T) {
|
|
323
|
+
return this.addProvider(component, props);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
addProvider<T = any>(component: ComponentType, props?: T) {
|
|
327
|
+
return this.providers.push([component, props]);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
addProviders(providers: (ComponentType | [ComponentType, any])[]) {
|
|
331
|
+
providers.forEach((provider) => {
|
|
332
|
+
if (Array.isArray(provider)) {
|
|
333
|
+
this.addProvider(provider[0], provider[1]);
|
|
334
|
+
} else {
|
|
335
|
+
this.addProvider(provider);
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async load() {
|
|
341
|
+
await this.loadWebSocket();
|
|
342
|
+
await this.pm.load();
|
|
343
|
+
await this.flowEngine.flowSettings.load();
|
|
344
|
+
this.updateFavicon();
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async loadWebSocket() {
|
|
348
|
+
this.eventBus.addEventListener('ws:message:authorized', () => {
|
|
349
|
+
this.setWsAuthorized(true);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
this.ws.on('message', (event) => {
|
|
353
|
+
if (!event.data) {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
const data = JSON.parse(event.data);
|
|
357
|
+
|
|
358
|
+
if (data?.payload?.refresh) {
|
|
359
|
+
window.location.reload();
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (data.type === 'notification') {
|
|
364
|
+
this.context.notification[data.payload?.type || 'info']({ message: data.payload?.message });
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (this.error && data.payload.code === 'APP_RUNNING') {
|
|
369
|
+
this.maintained = true;
|
|
370
|
+
this.setMaintaining(false);
|
|
371
|
+
this.error = null;
|
|
372
|
+
window.location.reload();
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const maintaining = data.type === 'maintaining' && data.payload.code !== 'APP_RUNNING';
|
|
377
|
+
console.log('ws:message', { maintaining, data });
|
|
378
|
+
if (maintaining) {
|
|
379
|
+
this.setMaintaining(true);
|
|
380
|
+
this.error = data.payload;
|
|
381
|
+
} else {
|
|
382
|
+
this.setMaintaining(false);
|
|
383
|
+
this.maintained = true;
|
|
384
|
+
this.error = null;
|
|
385
|
+
|
|
386
|
+
const type = data.type;
|
|
387
|
+
if (!type) {
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const eventName = `ws:message:${type}`;
|
|
392
|
+
this.eventBus.dispatchEvent(new CustomEvent(eventName, { detail: data.payload }));
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
this.ws.on('serverDown', () => {
|
|
397
|
+
this.maintaining = true;
|
|
398
|
+
this.maintained = false;
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
this.ws.on('open', () => {
|
|
402
|
+
const token = this.apiClient.auth.token;
|
|
403
|
+
|
|
404
|
+
if (token) {
|
|
405
|
+
this.setTokenInWebSocket({ token, authenticator: this.apiClient.auth.getAuthenticator() });
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
this.ws.connect();
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
getComponent<T = any>(Component: ComponentTypeAndString<T>, isShowError = true): ComponentType<T> | undefined {
|
|
413
|
+
const showError = (msg: string) => isShowError && console.error(msg);
|
|
414
|
+
if (!Component) {
|
|
415
|
+
showError(`getComponent called with ${Component}`);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// ClassComponent or FunctionComponent
|
|
420
|
+
if (typeof Component === 'function') return Component;
|
|
421
|
+
|
|
422
|
+
// Component is a string, try to get it from this.components
|
|
423
|
+
if (typeof Component === 'string') {
|
|
424
|
+
const res = _.get(this.components, Component) as ComponentType<T>;
|
|
425
|
+
if (!res) {
|
|
426
|
+
showError(`Component ${Component} not found`);
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
return res;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
showError(`Component ${Component} should be a string or a React component`);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
renderComponent<T extends {}>(Component: ComponentTypeAndString, props?: T, children?: ReactNode): ReactElement {
|
|
437
|
+
return React.createElement(this.getComponent(Component), props, children);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
private addComponent(component: ComponentType, name?: string) {
|
|
441
|
+
const componentName = name || component.displayName || component.name;
|
|
442
|
+
if (!componentName) {
|
|
443
|
+
console.error('Component must have a displayName or pass name as second argument');
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
_.set(this.components, componentName, component);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
addComponents(components: Record<string, ComponentType>) {
|
|
450
|
+
Object.keys(components).forEach((name) => {
|
|
451
|
+
this.addComponent(components[name], name);
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
getRootComponent() {
|
|
456
|
+
const Root: FC<{ children?: React.ReactNode }> = () => (
|
|
457
|
+
<FlowEngineProvider engine={this.flowEngine}>
|
|
458
|
+
<FlowModelRenderer fallback={this.renderComponent('AppSpin')} model={this.model} />
|
|
459
|
+
</FlowEngineProvider>
|
|
460
|
+
);
|
|
461
|
+
return Root;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
mount(containerOrSelector: Element | ShadowRoot | string) {
|
|
465
|
+
const container = normalizeContainer(containerOrSelector);
|
|
466
|
+
if (!container) return;
|
|
467
|
+
const App = this.getRootComponent();
|
|
468
|
+
const root = createRoot(container);
|
|
469
|
+
root.render(<App />);
|
|
470
|
+
return root;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
class ApplicationModel extends FlowModel {
|
|
475
|
+
#providers: ComponentType;
|
|
476
|
+
#router: any;
|
|
477
|
+
|
|
478
|
+
get app() {
|
|
479
|
+
return this.context.app as Application;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
getProviders() {
|
|
483
|
+
this.#providers = this.app.getComposeProviders();
|
|
484
|
+
return this.#providers;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
getRouter() {
|
|
488
|
+
this.#router = this.app.router.getRouterComponent();
|
|
489
|
+
return this.#router;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
render() {
|
|
493
|
+
console.log('render', {
|
|
494
|
+
maintaining: this.app.maintaining,
|
|
495
|
+
maintained: this.app.maintained,
|
|
496
|
+
error: this.app.error,
|
|
497
|
+
});
|
|
498
|
+
if (this.app.maintaining) {
|
|
499
|
+
return this.renderMaintaining();
|
|
500
|
+
}
|
|
501
|
+
if (this.app.error) {
|
|
502
|
+
return this.renderError();
|
|
503
|
+
}
|
|
504
|
+
return this.renderContent();
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
renderMaintaining() {
|
|
508
|
+
if (!this.app.maintained) {
|
|
509
|
+
return this.app.renderComponent('AppMaintaining');
|
|
510
|
+
}
|
|
511
|
+
return this.app.renderComponent('AppMaintainingDialog');
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
renderError() {
|
|
515
|
+
return this.app.renderComponent('AppError');
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
renderContent() {
|
|
519
|
+
const Router = this.getRouter();
|
|
520
|
+
const Providers = this.getProviders();
|
|
521
|
+
return <Router BaseLayout={Providers} />;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
ApplicationModel.registerFlow({
|
|
526
|
+
key: 'appFlow',
|
|
527
|
+
steps: {
|
|
528
|
+
init: {
|
|
529
|
+
async handler(ctx, params) {
|
|
530
|
+
try {
|
|
531
|
+
await ctx.app.load();
|
|
532
|
+
} catch (err) {
|
|
533
|
+
ctx.model.app.error = err;
|
|
534
|
+
console.error(err);
|
|
535
|
+
}
|
|
536
|
+
},
|
|
537
|
+
},
|
|
538
|
+
},
|
|
539
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
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 MockAdapter from 'axios-mock-adapter';
|
|
11
|
+
import { Application, type ApplicationOptions } from './Application';
|
|
12
|
+
import { WebSocketClient, WebSocketClientOptions } from './WebSocketClient';
|
|
13
|
+
|
|
14
|
+
class MockApplication extends Application {
|
|
15
|
+
apiMock: MockAdapter;
|
|
16
|
+
constructor(options: ApplicationOptions = {}) {
|
|
17
|
+
super({
|
|
18
|
+
router: { type: 'memory', initialEntries: ['/'] },
|
|
19
|
+
...options,
|
|
20
|
+
});
|
|
21
|
+
this.apiMock = new MockAdapter(this.apiClient.axios);
|
|
22
|
+
this.ws = new MockWebSocketClient(options.ws || {});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
class MockWebSocketClient extends WebSocketClient {
|
|
27
|
+
public eventBus = new EventTarget();
|
|
28
|
+
constructor(options: WebSocketClientOptions | boolean) {
|
|
29
|
+
super(options);
|
|
30
|
+
}
|
|
31
|
+
connect() {}
|
|
32
|
+
close() {}
|
|
33
|
+
send() {}
|
|
34
|
+
on(type: string, listener, options?: boolean | AddEventListenerOptions) {
|
|
35
|
+
this.eventBus.addEventListener(
|
|
36
|
+
type,
|
|
37
|
+
(event: CustomEvent) => listener({ data: JSON.stringify(event.detail) }),
|
|
38
|
+
options,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
emit(type: string, data: any) {
|
|
42
|
+
this.eventBus.dispatchEvent(new CustomEvent(type, { detail: data }));
|
|
43
|
+
}
|
|
44
|
+
off(type: string, listener: EventListener, options?: boolean | EventListenerOptions) {
|
|
45
|
+
this.eventBus.removeEventListener(type, listener, options);
|
|
46
|
+
}
|
|
47
|
+
removeAllListeners() {}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function createMockClient(options?: ApplicationOptions) {
|
|
51
|
+
const app = new MockApplication(options);
|
|
52
|
+
return app;
|
|
53
|
+
}
|
package/src/Plugin.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
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 type { TFuncKey, TOptions } from 'i18next';
|
|
11
|
+
import type { Application } from './Application';
|
|
12
|
+
|
|
13
|
+
export class Plugin<T = any> {
|
|
14
|
+
constructor(
|
|
15
|
+
public options: T,
|
|
16
|
+
protected app: Application,
|
|
17
|
+
) {
|
|
18
|
+
this.options = options;
|
|
19
|
+
this.app = app;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
get pluginManager() {
|
|
23
|
+
return this.app.pluginManager;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
get context() {
|
|
27
|
+
return this.app.context;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
get flowEngine() {
|
|
31
|
+
return this.app.flowEngine;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
get engine() {
|
|
35
|
+
return this.app.flowEngine;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
get pm() {
|
|
39
|
+
return this.app.pm;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get router() {
|
|
43
|
+
return this.app.router;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
get pluginSettingsManager() {
|
|
47
|
+
return this.app.pluginSettingsManager;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
get pluginSettingsRouter() {
|
|
51
|
+
return this.app.pluginSettingsManager;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
get schemaInitializerManager() {
|
|
55
|
+
// @ts-ignore
|
|
56
|
+
return this.app.schemaInitializerManager;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
get schemaSettingsManager() {
|
|
60
|
+
// @ts-ignore
|
|
61
|
+
return this.app.schemaSettingsManager;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
get dataSourceManager() {
|
|
65
|
+
// @ts-ignore
|
|
66
|
+
return this.app.dataSourceManager;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async afterAdd() {}
|
|
70
|
+
|
|
71
|
+
async beforeLoad() {}
|
|
72
|
+
|
|
73
|
+
async load() {}
|
|
74
|
+
|
|
75
|
+
t(text: TFuncKey | TFuncKey[], options: TOptions = {}) {
|
|
76
|
+
return this.app.i18n.t(text, { ns: this.options?.['packageName'], ...(options as any) });
|
|
77
|
+
}
|
|
78
|
+
}
|