@nocobase/client-v2 2.1.0-beta.37 → 2.1.0-beta.38
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/Application.d.ts +1 -0
- package/es/BaseApplication.d.ts +3 -0
- package/es/RouterManager.d.ts +1 -0
- package/es/components/KeepAlive.d.ts +22 -0
- package/es/components/RouterBridge.d.ts +9 -0
- package/es/data-source/ExtendCollectionsProvider.d.ts +28 -2
- package/es/flow/FlowPage.d.ts +2 -1
- package/es/flow/admin-shell/AdminLayoutRouteCoordinator.d.ts +8 -40
- package/es/flow/admin-shell/BaseLayoutModel.d.ts +89 -0
- package/es/flow/admin-shell/BaseLayoutRouteCoordinator.d.ts +74 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.d.ts +12 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutModel.d.ts +7 -92
- package/es/flow/admin-shell/admin-layout/index.d.ts +2 -0
- package/es/flow/admin-shell/useAdminLayoutRoutePage.d.ts +2 -2
- package/es/flow/admin-shell/useLayoutRoutePage.d.ts +23 -0
- package/es/flow/components/FlowRoute.d.ts +10 -1
- package/es/flow/index.d.ts +4 -0
- package/es/flow/models/base/PageModel/PageModel.d.ts +3 -1
- package/es/flow/models/blocks/form/FormActionGroupModel.d.ts +1 -0
- package/es/flow/models/blocks/table/TableBlockModel.d.ts +10 -0
- package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.d.ts +1 -1
- package/es/index.d.ts +1 -0
- package/es/index.mjs +484 -437
- package/es/layout-manager/LayoutContentRoute.d.ts +14 -0
- package/es/layout-manager/LayoutManager.d.ts +22 -0
- package/es/layout-manager/LayoutRoute.d.ts +14 -0
- package/es/layout-manager/index.d.ts +13 -0
- package/es/layout-manager/types.d.ts +20 -0
- package/es/layout-manager/utils.d.ts +14 -0
- package/es/settings-center/index.d.ts +1 -1
- package/es/settings-center/plugin-manager/BulkEnableButton.d.ts +15 -0
- package/es/settings-center/plugin-manager/PluginCard.d.ts +15 -0
- package/es/settings-center/plugin-manager/PluginDetail.d.ts +16 -0
- package/es/settings-center/{PluginManagerPage.d.ts → plugin-manager/index.d.ts} +1 -7
- package/es/settings-center/plugin-manager/types.d.ts +34 -0
- package/lib/index.js +484 -437
- package/package.json +8 -7
- package/src/Application.tsx +27 -12
- package/src/BaseApplication.tsx +6 -0
- package/src/PluginSettingsManager.ts +1 -1
- package/src/RouterManager.tsx +17 -1
- package/src/__tests__/PluginSettingsManager.test.ts +41 -2
- package/src/__tests__/app.test.tsx +8 -1
- package/src/__tests__/globalDeps.test.ts +1 -0
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +45 -2
- package/src/__tests__/plugin-manager.test.tsx +177 -0
- package/src/__tests__/settings-center.test.tsx +24 -2
- package/src/components/KeepAlive.tsx +131 -0
- package/src/components/RouterBridge.tsx +28 -4
- package/src/components/__tests__/KeepAlive.test.tsx +63 -0
- package/src/components/__tests__/RouterBridge.test.tsx +27 -0
- package/src/data-source/ExtendCollectionsProvider.tsx +94 -20
- package/src/data-source/__tests__/ExtendCollectionsProvider.test.tsx +264 -0
- package/src/flow/FlowPage.tsx +35 -7
- package/src/flow/__tests__/FlowPage.test.tsx +79 -0
- package/src/flow/__tests__/FlowRoute.test.tsx +529 -2
- package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +191 -0
- package/src/flow/actions/__tests__/openView.subModelKey.test.tsx +33 -0
- package/src/flow/actions/aclCheck.tsx +4 -0
- package/src/flow/actions/aclCheckRefresh.tsx +4 -0
- package/src/flow/actions/dateTimeFormat.tsx +12 -8
- package/src/flow/actions/linkageRules.tsx +122 -0
- package/src/flow/actions/openView.tsx +28 -4
- package/src/flow/admin-shell/AdminLayoutRouteCoordinator.ts +11 -329
- package/src/flow/admin-shell/BaseLayoutModel.tsx +455 -0
- package/src/flow/admin-shell/BaseLayoutRouteCoordinator.ts +502 -0
- package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +547 -3
- package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +4 -4
- package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +160 -0
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +0 -12
- package/src/flow/admin-shell/admin-layout/AdminLayoutModel.tsx +28 -201
- package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +11 -2
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +1 -26
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +149 -27
- package/src/flow/admin-shell/admin-layout/index.ts +2 -0
- package/src/flow/admin-shell/useAdminLayoutRoutePage.ts +10 -26
- package/src/flow/admin-shell/useLayoutRoutePage.ts +61 -0
- package/src/flow/components/AdminLayout.tsx +4 -154
- package/src/flow/components/FlowRoute.tsx +105 -15
- package/src/flow/index.ts +4 -0
- package/src/flow/models/base/ActionModel.tsx +8 -1
- package/src/flow/models/base/PageModel/PageModel.tsx +51 -18
- package/src/flow/models/base/PageModel/RootPageModel.tsx +6 -13
- package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +102 -1
- package/src/flow/models/base/RouteModel.tsx +1 -1
- package/src/flow/models/blocks/form/FormActionGroupModel.tsx +14 -0
- package/src/flow/models/blocks/form/FormItemModel.tsx +8 -1
- package/src/flow/models/blocks/form/__tests__/FormActionGroupModel.test.ts +46 -0
- package/src/flow/models/blocks/form/submitValues.ts +4 -1
- package/src/flow/models/blocks/table/TableBlockModel.tsx +118 -16
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.rowSelection.test.tsx +114 -0
- package/src/flow/models/fields/AssociationFieldModel/SubFormFieldModel.tsx +7 -1
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +1 -1
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +6 -5
- package/src/flow/models/fields/ClickableFieldModel.tsx +9 -1
- package/src/flow/models/fields/DisplayTimeFieldModel.tsx +1 -1
- package/src/flow/models/fields/TimeFieldModel.tsx +1 -1
- package/src/flow/models/fields/__tests__/TimeFieldModel.test.tsx +61 -0
- package/src/flow/models/fields/mobile-components/MobileDatePicker.tsx +19 -3
- package/src/flow/models/fields/mobile-components/__tests__/MobileDatePicker.test.tsx +94 -0
- package/src/flow/models/topbar/TopbarActionModel.tsx +1 -1
- package/src/flow/utils/__tests__/dateTimeFormat.test.ts +91 -0
- package/src/index.ts +1 -0
- package/src/layout-manager/LayoutContentRoute.tsx +90 -0
- package/src/layout-manager/LayoutManager.tsx +185 -0
- package/src/layout-manager/LayoutRoute.tsx +138 -0
- package/src/layout-manager/__tests__/LayoutManager.test.tsx +335 -0
- package/src/layout-manager/__tests__/LayoutRoute.test.tsx +473 -0
- package/src/layout-manager/index.ts +14 -0
- package/src/layout-manager/types.ts +22 -0
- package/src/layout-manager/utils.ts +37 -0
- package/src/nocobase-buildin-plugin/index.tsx +56 -48
- package/src/settings-center/index.ts +1 -1
- package/src/settings-center/plugin-manager/BulkEnableButton.tsx +111 -0
- package/src/settings-center/plugin-manager/PluginCard.tsx +270 -0
- package/src/settings-center/plugin-manager/PluginDetail.tsx +195 -0
- package/src/settings-center/plugin-manager/index.tsx +254 -0
- package/src/settings-center/plugin-manager/types.ts +35 -0
- package/src/settings-center/utils.tsx +8 -1
- package/src/theme/__tests__/globalStyles.test.ts +24 -0
- package/src/theme/globalStyles.ts +10 -0
- package/src/utils/globalDeps.ts +2 -0
- package/src/settings-center/PluginManagerPage.tsx +0 -162
|
@@ -15,7 +15,7 @@ import type { Application } from '../Application';
|
|
|
15
15
|
import { getCurrentV2RedirectPath, getDefaultV2AdminRedirectPath } from '../authRedirect';
|
|
16
16
|
import { AppNotFound } from '../components';
|
|
17
17
|
import { PluginFlowEngine } from '../flow';
|
|
18
|
-
import { AdminLayoutMenuItemModel, AdminLayoutModel } from '../flow/admin-shell/admin-layout';
|
|
18
|
+
import { ADMIN_LAYOUT_MODEL_UID, AdminLayoutMenuItemModel, AdminLayoutModel } from '../flow/admin-shell/admin-layout';
|
|
19
19
|
import { useApp } from '../hooks/useApp';
|
|
20
20
|
import { Plugin } from '../Plugin';
|
|
21
21
|
import { AdminSettingsLayoutModel } from '../settings-center';
|
|
@@ -56,6 +56,30 @@ function isAdminRuntimeRoute(pathname: string, basename?: string) {
|
|
|
56
56
|
return normalizedPathname === '/admin' || normalizedPathname.startsWith('/admin/');
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
function hasAuthCheckRoute(app: Application, pathname: string) {
|
|
60
|
+
const matchedRoutes = app.router.matchRoutes(pathname) || [];
|
|
61
|
+
return matchedRoutes.some((match) => match?.route?.authCheck === true);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function shouldCheckRuntimeRoute(app: Application, pathname: string) {
|
|
65
|
+
return isAdminRuntimeRoute(pathname, app.router.getBasename()) || hasAuthCheckRoute(app, pathname);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
type CurrentUserAuthCheckRouteState = 'skipped' | 'unchecked' | 'required';
|
|
69
|
+
|
|
70
|
+
function getCurrentUserAuthCheckRouteState(app: Application, pathname: string): CurrentUserAuthCheckRouteState {
|
|
71
|
+
const basename = app.router.getBasename();
|
|
72
|
+
if (isBuiltinAuthRoute(pathname, basename) || app.router.isSkippedAuthCheckRoute(pathname)) {
|
|
73
|
+
return 'skipped';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!shouldCheckRuntimeRoute(app, pathname)) {
|
|
77
|
+
return 'unchecked';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return 'required';
|
|
81
|
+
}
|
|
82
|
+
|
|
59
83
|
export const CurrentUserContext = createContext<CurrentUserState | null>(null);
|
|
60
84
|
CurrentUserContext.displayName = 'CurrentUserContext';
|
|
61
85
|
|
|
@@ -87,7 +111,7 @@ export function useCurrentRoles(): CurrentRoleOption[] {
|
|
|
87
111
|
}
|
|
88
112
|
|
|
89
113
|
const DataSourceBootstrapProvider: FC = ({ children }) => {
|
|
90
|
-
const app = useApp();
|
|
114
|
+
const app = useApp<Application>();
|
|
91
115
|
const location = useLocation();
|
|
92
116
|
const [loading, setLoading] = useState(true);
|
|
93
117
|
const [error, setError] = useState<Error | null>(null);
|
|
@@ -98,7 +122,7 @@ const DataSourceBootstrapProvider: FC = ({ children }) => {
|
|
|
98
122
|
const basename = app.router.getBasename();
|
|
99
123
|
const isSkippedAuthCheckRoute =
|
|
100
124
|
isBuiltinAuthRoute(location.pathname, basename) || app.router.isSkippedAuthCheckRoute(location.pathname);
|
|
101
|
-
const shouldBootstrap =
|
|
125
|
+
const shouldBootstrap = shouldCheckRuntimeRoute(app, location.pathname);
|
|
102
126
|
|
|
103
127
|
if (isSkippedAuthCheckRoute || !shouldBootstrap) {
|
|
104
128
|
setLoading(false);
|
|
@@ -146,28 +170,25 @@ const DataSourceBootstrapProvider: FC = ({ children }) => {
|
|
|
146
170
|
};
|
|
147
171
|
|
|
148
172
|
const CurrentUserProvider: FC = ({ children }) => {
|
|
149
|
-
const app = useApp();
|
|
173
|
+
const app = useApp<Application>();
|
|
150
174
|
const location = useLocation();
|
|
151
175
|
const navigate = useNavigate();
|
|
152
176
|
const [state, setState] = useState<CurrentUserState>({ loading: true });
|
|
153
|
-
const pathnameRef = useRef(location.pathname);
|
|
154
|
-
pathnameRef.current = location.pathname;
|
|
155
177
|
const locationRef = useRef(location);
|
|
156
178
|
locationRef.current = location;
|
|
179
|
+
const authCheckRouteState = getCurrentUserAuthCheckRouteState(app, location.pathname);
|
|
157
180
|
|
|
158
181
|
useEffect(() => {
|
|
159
182
|
let mounted = true;
|
|
160
|
-
const isSkippedAuthCheckRoute =
|
|
161
|
-
isBuiltinAuthRoute(pathnameRef.current, app.router.getBasename()) ||
|
|
162
|
-
app.router.isSkippedAuthCheckRoute(pathnameRef.current);
|
|
163
|
-
const shouldCheckCurrentUser = isAdminRuntimeRoute(pathnameRef.current, app.router.getBasename());
|
|
164
183
|
|
|
165
|
-
if (
|
|
184
|
+
if (authCheckRouteState !== 'required') {
|
|
166
185
|
// 认证页等免鉴权路由不应再执行 `/auth:check`,否则未登录时会重复鉴权并触发重定向抖动。
|
|
167
186
|
setState({ loading: false });
|
|
168
187
|
return;
|
|
169
188
|
}
|
|
170
189
|
|
|
190
|
+
setState((previous) => (previous.loading ? previous : { ...previous, loading: true }));
|
|
191
|
+
|
|
171
192
|
const run = async () => {
|
|
172
193
|
try {
|
|
173
194
|
const res = await app.apiClient.request({
|
|
@@ -176,12 +197,14 @@ const CurrentUserProvider: FC = ({ children }) => {
|
|
|
176
197
|
skipAuth: true,
|
|
177
198
|
});
|
|
178
199
|
|
|
200
|
+
if (!mounted) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
179
204
|
const user = res?.data?.data;
|
|
180
205
|
// 服务端通过 `{ code: 302, redirect }` 通知客户端先去某个中间页(例如 2FA 验证页)。这类响应没有 user.id,但也不能视为未登录——否则会和处理 302 的全局响应拦截器 (例如 plugin-two-factor-authentication 注册的那一个)竞态,而 `window.location.replace` 会覆盖更早发出的 `window.location.href`,把用户错误地弹回登录页。让响应拦截器接管跳转。
|
|
181
206
|
if (user?.code === 302) {
|
|
182
|
-
|
|
183
|
-
setState({ loading: false });
|
|
184
|
-
}
|
|
207
|
+
setState({ loading: false });
|
|
185
208
|
return;
|
|
186
209
|
}
|
|
187
210
|
if (user?.id == null) {
|
|
@@ -204,13 +227,15 @@ const CurrentUserProvider: FC = ({ children }) => {
|
|
|
204
227
|
meta: userMeta,
|
|
205
228
|
});
|
|
206
229
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
});
|
|
212
|
-
}
|
|
230
|
+
setState({
|
|
231
|
+
data: res?.data,
|
|
232
|
+
loading: false,
|
|
233
|
+
});
|
|
213
234
|
} catch (error: any) {
|
|
235
|
+
if (!mounted) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
214
239
|
const isAuthError = error?.response?.status === 401 || error?.status === 401;
|
|
215
240
|
if (isAuthError) {
|
|
216
241
|
navigate(`/signin?redirect=${encodeURIComponent(getCurrentV2RedirectPath(app, locationRef.current))}`, {
|
|
@@ -218,19 +243,17 @@ const CurrentUserProvider: FC = ({ children }) => {
|
|
|
218
243
|
});
|
|
219
244
|
return;
|
|
220
245
|
}
|
|
221
|
-
|
|
222
|
-
setState({ loading: false });
|
|
223
|
-
}
|
|
246
|
+
setState({ loading: false });
|
|
224
247
|
throw error;
|
|
225
248
|
}
|
|
226
249
|
};
|
|
227
250
|
|
|
228
|
-
|
|
251
|
+
run();
|
|
229
252
|
|
|
230
253
|
return () => {
|
|
231
254
|
mounted = false;
|
|
232
255
|
};
|
|
233
|
-
}, [app, navigate]);
|
|
256
|
+
}, [app, authCheckRouteState, navigate]);
|
|
234
257
|
|
|
235
258
|
if (state.loading) {
|
|
236
259
|
return app.renderComponent('AppSpin');
|
|
@@ -240,7 +263,7 @@ const CurrentUserProvider: FC = ({ children }) => {
|
|
|
240
263
|
};
|
|
241
264
|
|
|
242
265
|
const RootRedirect: FC = () => {
|
|
243
|
-
const app = useApp();
|
|
266
|
+
const app = useApp<Application>();
|
|
244
267
|
const hasToken = !!app?.apiClient?.auth?.token;
|
|
245
268
|
const targetPath = getDefaultV2AdminRedirectPath(app);
|
|
246
269
|
|
|
@@ -273,6 +296,12 @@ export class NocoBaseBuildInPlugin extends Plugin<any, Application> {
|
|
|
273
296
|
AdminLayoutMenuItemModel,
|
|
274
297
|
AdminSettingsLayoutModel,
|
|
275
298
|
});
|
|
299
|
+
this.app.layoutManager.registerLayout({
|
|
300
|
+
routeName: 'admin',
|
|
301
|
+
routePath: '/admin',
|
|
302
|
+
uid: ADMIN_LAYOUT_MODEL_UID,
|
|
303
|
+
layoutModelClass: 'AdminLayoutModel',
|
|
304
|
+
});
|
|
276
305
|
|
|
277
306
|
this.app.pluginSettingsManager.addMenuItem({
|
|
278
307
|
key: 'plugin-manager',
|
|
@@ -285,7 +314,7 @@ export class NocoBaseBuildInPlugin extends Plugin<any, Application> {
|
|
|
285
314
|
menuKey: 'plugin-manager',
|
|
286
315
|
key: 'index',
|
|
287
316
|
title: this.app.i18n.t('Plugin manager'),
|
|
288
|
-
componentLoader: () => import('../settings-center/
|
|
317
|
+
componentLoader: () => import('../settings-center/plugin-manager'),
|
|
289
318
|
aclSnippet: 'pm',
|
|
290
319
|
sort: -200,
|
|
291
320
|
});
|
|
@@ -324,10 +353,6 @@ export class NocoBaseBuildInPlugin extends Plugin<any, Application> {
|
|
|
324
353
|
Component: AppNotFound,
|
|
325
354
|
});
|
|
326
355
|
|
|
327
|
-
this.router.add('admin', {
|
|
328
|
-
path: '/admin',
|
|
329
|
-
componentLoader: () => import('../flow/components/AdminLayout'),
|
|
330
|
-
});
|
|
331
356
|
this.router.add('admin.settings', {
|
|
332
357
|
path: '/admin/settings',
|
|
333
358
|
componentLoader: () => import('../settings-center/AdminSettingsLayout'),
|
|
@@ -336,23 +361,6 @@ export class NocoBaseBuildInPlugin extends Plugin<any, Application> {
|
|
|
336
361
|
path: '*',
|
|
337
362
|
Component: Outlet,
|
|
338
363
|
});
|
|
339
|
-
this.router.add('admin.page', {
|
|
340
|
-
path: '/admin/:name',
|
|
341
|
-
componentLoader: () => import('../flow/components/FlowRoute'),
|
|
342
|
-
});
|
|
343
|
-
|
|
344
|
-
this.router.add('admin.page.tab', {
|
|
345
|
-
path: '/admin/:name/tab/:tabUid',
|
|
346
|
-
componentLoader: () => import('../flow/components/FlowRoute'),
|
|
347
|
-
});
|
|
348
|
-
this.router.add('admin.page.view', {
|
|
349
|
-
path: '/admin/:name/view/*',
|
|
350
|
-
componentLoader: () => import('../flow/components/FlowRoute'),
|
|
351
|
-
});
|
|
352
|
-
this.router.add('admin.page.tab.view', {
|
|
353
|
-
path: '/admin/:name/tab/:tabUid/view/*',
|
|
354
|
-
componentLoader: () => import('../flow/components/FlowRoute'),
|
|
355
|
-
});
|
|
356
364
|
}
|
|
357
365
|
|
|
358
366
|
addComponents() {}
|
|
@@ -0,0 +1,111 @@
|
|
|
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 { useMemoizedFn } from 'ahooks';
|
|
11
|
+
import { Button, Input, Modal, Table } from 'antd';
|
|
12
|
+
import type { TableProps } from 'antd';
|
|
13
|
+
import _ from 'lodash';
|
|
14
|
+
import React, { useMemo, useState } from 'react';
|
|
15
|
+
import { useTranslation } from 'react-i18next';
|
|
16
|
+
import { useApp } from '../../hooks/useApp';
|
|
17
|
+
import type { IPluginData } from './types';
|
|
18
|
+
|
|
19
|
+
interface BulkEnableButtonProps {
|
|
20
|
+
plugins: IPluginData[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const BulkEnableButton: React.FC<BulkEnableButtonProps> = ({ plugins }) => {
|
|
24
|
+
const { t } = useTranslation();
|
|
25
|
+
const app = useApp();
|
|
26
|
+
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
27
|
+
const [searchValue, setSearchValue] = useState('');
|
|
28
|
+
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
|
29
|
+
|
|
30
|
+
const disabledPlugins = useMemo(() => plugins.filter((plugin) => !plugin.enabled), [plugins]);
|
|
31
|
+
|
|
32
|
+
const items = useMemo(() => {
|
|
33
|
+
const value = searchValue.toLowerCase().trim();
|
|
34
|
+
if (!value) return disabledPlugins;
|
|
35
|
+
return disabledPlugins.filter(
|
|
36
|
+
(plugin) =>
|
|
37
|
+
(plugin.displayName || '').toLowerCase().includes(value) ||
|
|
38
|
+
(plugin.description || '').toLowerCase().includes(value),
|
|
39
|
+
);
|
|
40
|
+
}, [disabledPlugins, searchValue]);
|
|
41
|
+
|
|
42
|
+
const handleOpen = useMemoizedFn(() => setIsModalOpen(true));
|
|
43
|
+
|
|
44
|
+
const handleCancel = useMemoizedFn(() => {
|
|
45
|
+
setSelectedRowKeys([]);
|
|
46
|
+
setIsModalOpen(false);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const handleOk = useMemoizedFn(async () => {
|
|
50
|
+
await app.apiClient.request({
|
|
51
|
+
url: 'pm:enable',
|
|
52
|
+
params: { filterByTk: selectedRowKeys },
|
|
53
|
+
});
|
|
54
|
+
setIsModalOpen(false);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const columns: TableProps<IPluginData>['columns'] = useMemo(
|
|
58
|
+
() => [
|
|
59
|
+
{ title: t('Plugin'), dataIndex: 'displayName', ellipsis: true },
|
|
60
|
+
{ title: t('Description'), dataIndex: 'description', ellipsis: true, width: 300 },
|
|
61
|
+
{ title: t('Package name'), dataIndex: 'packageName', width: 300, ellipsis: true },
|
|
62
|
+
],
|
|
63
|
+
[t],
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<>
|
|
68
|
+
<Button onClick={handleOpen}>{t('Bulk enable')}</Button>
|
|
69
|
+
<Modal width={1000} title={t('Bulk enable')} open={isModalOpen} onOk={handleOk} onCancel={handleCancel}>
|
|
70
|
+
<Input
|
|
71
|
+
style={{ marginBottom: '1em' }}
|
|
72
|
+
placeholder={t('Search plugin...')}
|
|
73
|
+
allowClear
|
|
74
|
+
onChange={(e) => setSearchValue(e.target.value)}
|
|
75
|
+
/>
|
|
76
|
+
<Table<IPluginData>
|
|
77
|
+
rowSelection={{
|
|
78
|
+
type: 'checkbox',
|
|
79
|
+
selectedRowKeys,
|
|
80
|
+
onChange(selectedKeys) {
|
|
81
|
+
const names = items.map((item) => item.name);
|
|
82
|
+
setSelectedRowKeys((preSelectedRowKeys) => {
|
|
83
|
+
if (selectedKeys.length === 0) {
|
|
84
|
+
return preSelectedRowKeys.filter((key) => !names.includes(String(key)));
|
|
85
|
+
}
|
|
86
|
+
if (selectedKeys.length === names.length) {
|
|
87
|
+
return _.uniq([...preSelectedRowKeys, ...selectedKeys]);
|
|
88
|
+
}
|
|
89
|
+
return preSelectedRowKeys;
|
|
90
|
+
});
|
|
91
|
+
},
|
|
92
|
+
onSelect(record) {
|
|
93
|
+
setSelectedRowKeys((preSelectedRowKeys) => {
|
|
94
|
+
if (preSelectedRowKeys.includes(record.name)) {
|
|
95
|
+
return preSelectedRowKeys.filter((key) => key !== record.name);
|
|
96
|
+
}
|
|
97
|
+
return preSelectedRowKeys.concat(record.name);
|
|
98
|
+
});
|
|
99
|
+
},
|
|
100
|
+
}}
|
|
101
|
+
rowKey="name"
|
|
102
|
+
scroll={{ y: '60vh' }}
|
|
103
|
+
size="small"
|
|
104
|
+
pagination={false}
|
|
105
|
+
columns={columns}
|
|
106
|
+
dataSource={items}
|
|
107
|
+
/>
|
|
108
|
+
</Modal>
|
|
109
|
+
</>
|
|
110
|
+
);
|
|
111
|
+
};
|
|
@@ -0,0 +1,270 @@
|
|
|
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 {
|
|
11
|
+
CloseCircleFilled,
|
|
12
|
+
DeleteOutlined,
|
|
13
|
+
LoadingOutlined,
|
|
14
|
+
ReadOutlined,
|
|
15
|
+
SettingOutlined,
|
|
16
|
+
WarningFilled,
|
|
17
|
+
} from '@ant-design/icons';
|
|
18
|
+
import { css } from '@emotion/css';
|
|
19
|
+
import { useMemoizedFn } from 'ahooks';
|
|
20
|
+
import { App, Card, Divider, Modal, Popconfirm, Result, Space, Switch, Tooltip, Typography, theme } from 'antd';
|
|
21
|
+
import React, { FC, useMemo, useState } from 'react';
|
|
22
|
+
import { useTranslation } from 'react-i18next';
|
|
23
|
+
import { useNavigate } from 'react-router-dom';
|
|
24
|
+
import { useApp } from '../../hooks/useApp';
|
|
25
|
+
import { PluginDetail } from './PluginDetail';
|
|
26
|
+
import type { IPluginData } from './types';
|
|
27
|
+
|
|
28
|
+
interface PluginCardProps {
|
|
29
|
+
data: IPluginData;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const PluginCard: FC<PluginCardProps> = ({ data }) => {
|
|
33
|
+
const { token } = theme.useToken();
|
|
34
|
+
const { t } = useTranslation();
|
|
35
|
+
const navigate = useNavigate();
|
|
36
|
+
const app = useApp();
|
|
37
|
+
const { modal } = App.useApp();
|
|
38
|
+
const [detailOpen, setDetailOpen] = useState(false);
|
|
39
|
+
|
|
40
|
+
const { name, displayName, isCompatible, packageName, builtIn, enabled, removable, description, error, homepage } =
|
|
41
|
+
data;
|
|
42
|
+
|
|
43
|
+
const title = displayName || name || packageName;
|
|
44
|
+
|
|
45
|
+
const openDetail = useMemoizedFn(() => {
|
|
46
|
+
if (!error) {
|
|
47
|
+
setDetailOpen(true);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const handleEnable = useMemoizedFn(async () => {
|
|
52
|
+
await app.apiClient.request({
|
|
53
|
+
url: 'pm:enable',
|
|
54
|
+
params: { filterByTk: name },
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const handleDisable = useMemoizedFn(async () => {
|
|
59
|
+
await app.apiClient.request({
|
|
60
|
+
url: 'pm:disable',
|
|
61
|
+
params: { filterByTk: name },
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const handleRemove = useMemoizedFn(async () => {
|
|
66
|
+
await app.apiClient.request({
|
|
67
|
+
url: 'pm:remove',
|
|
68
|
+
params: { filterByTk: name },
|
|
69
|
+
});
|
|
70
|
+
modal.info({
|
|
71
|
+
icon: null,
|
|
72
|
+
width: 520,
|
|
73
|
+
content: (
|
|
74
|
+
<Result
|
|
75
|
+
icon={<LoadingOutlined />}
|
|
76
|
+
title={t('Plugin removing')}
|
|
77
|
+
subTitle={t('Plugin is removing, please wait...')}
|
|
78
|
+
/>
|
|
79
|
+
),
|
|
80
|
+
footer: null,
|
|
81
|
+
});
|
|
82
|
+
const checkHealth = () => {
|
|
83
|
+
app.apiClient
|
|
84
|
+
.request({ url: '__health_check', method: 'get', skipNotify: true })
|
|
85
|
+
.then((response) => {
|
|
86
|
+
if (response?.data === 'ok') {
|
|
87
|
+
window.location.reload();
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
.catch(() => {
|
|
91
|
+
// health check still failing, keep polling
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
setInterval(checkHealth, 1000);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const handleSwitchChange = useMemoizedFn(async (checked: boolean) => {
|
|
98
|
+
if (!isCompatible && checked) {
|
|
99
|
+
modal.confirm({
|
|
100
|
+
title: t('Plugin dependency version mismatch'),
|
|
101
|
+
content: t(
|
|
102
|
+
'The current dependency version of the plugin does not match the version of the application and may not work properly. Are you sure you want to continue enabling the plugin?',
|
|
103
|
+
),
|
|
104
|
+
onOk: handleEnable,
|
|
105
|
+
});
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (!checked) {
|
|
109
|
+
modal.confirm({
|
|
110
|
+
title: t('Are you sure to disable this plugin?'),
|
|
111
|
+
onOk: handleDisable,
|
|
112
|
+
});
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
await handleEnable();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const openSettings = useMemoizedFn(() => {
|
|
119
|
+
navigate(app.pluginSettingsManager.getRoutePath(name));
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const cardClassName = useMemo(
|
|
123
|
+
() => css`
|
|
124
|
+
display: flex;
|
|
125
|
+
flex-direction: column;
|
|
126
|
+
height: 100%;
|
|
127
|
+
|
|
128
|
+
.ant-card-body {
|
|
129
|
+
flex-grow: 1;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.ant-card-actions {
|
|
133
|
+
li .ant-space {
|
|
134
|
+
gap: ${token.marginXXS}px !important;
|
|
135
|
+
}
|
|
136
|
+
li:first-child {
|
|
137
|
+
width: 80% !important;
|
|
138
|
+
border-inline-end: 0;
|
|
139
|
+
text-align: left;
|
|
140
|
+
padding-inline-start: ${token.padding}px;
|
|
141
|
+
}
|
|
142
|
+
li:last-child {
|
|
143
|
+
width: 20% !important;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
`,
|
|
147
|
+
[token.marginXXS, token.padding],
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const cardTitle = (
|
|
151
|
+
<Space>
|
|
152
|
+
{error ? (
|
|
153
|
+
<Tooltip title={t('Plugin loading failed. Please check the server logs.')}>
|
|
154
|
+
<Typography.Text type="danger">
|
|
155
|
+
<CloseCircleFilled />
|
|
156
|
+
</Typography.Text>
|
|
157
|
+
</Tooltip>
|
|
158
|
+
) : null}
|
|
159
|
+
{!isCompatible ? (
|
|
160
|
+
<Tooltip title={t('Plugin dependencies check failed')}>
|
|
161
|
+
<Typography.Text type="warning">
|
|
162
|
+
<WarningFilled />
|
|
163
|
+
</Typography.Text>
|
|
164
|
+
</Tooltip>
|
|
165
|
+
) : null}
|
|
166
|
+
<span>{title}</span>
|
|
167
|
+
</Space>
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const linksAction = (
|
|
171
|
+
<Space split={<Divider type="vertical" />} key="links" size={token.marginXXS}>
|
|
172
|
+
{homepage && (
|
|
173
|
+
<a
|
|
174
|
+
href={homepage}
|
|
175
|
+
target="_blank"
|
|
176
|
+
rel="noreferrer"
|
|
177
|
+
onClick={(event) => event.stopPropagation()}
|
|
178
|
+
aria-label={t('Docs')}
|
|
179
|
+
>
|
|
180
|
+
<ReadOutlined /> {t('Docs')}
|
|
181
|
+
</a>
|
|
182
|
+
)}
|
|
183
|
+
{enabled && app.pluginSettingsManager.has(name) && (
|
|
184
|
+
<a
|
|
185
|
+
onClick={(e) => {
|
|
186
|
+
e.stopPropagation();
|
|
187
|
+
openSettings();
|
|
188
|
+
}}
|
|
189
|
+
aria-label={t('Settings')}
|
|
190
|
+
>
|
|
191
|
+
<SettingOutlined /> {t('Settings')}
|
|
192
|
+
</a>
|
|
193
|
+
)}
|
|
194
|
+
{removable && (
|
|
195
|
+
<Popconfirm
|
|
196
|
+
key="delete"
|
|
197
|
+
disabled={builtIn}
|
|
198
|
+
title={t('Are you sure to delete this plugin?')}
|
|
199
|
+
onConfirm={(e) => {
|
|
200
|
+
e?.stopPropagation();
|
|
201
|
+
handleRemove();
|
|
202
|
+
}}
|
|
203
|
+
onCancel={(e) => e?.stopPropagation()}
|
|
204
|
+
okText={t('Yes')}
|
|
205
|
+
cancelText={t('No')}
|
|
206
|
+
>
|
|
207
|
+
<a
|
|
208
|
+
onClick={(e) => e.stopPropagation()}
|
|
209
|
+
aria-label={t('Remove')}
|
|
210
|
+
style={builtIn ? { color: token.colorTextDisabled, cursor: 'not-allowed' } : undefined}
|
|
211
|
+
>
|
|
212
|
+
<DeleteOutlined /> {t('Remove')}
|
|
213
|
+
</a>
|
|
214
|
+
</Popconfirm>
|
|
215
|
+
)}
|
|
216
|
+
</Space>
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
const switchAction = (
|
|
220
|
+
<Switch
|
|
221
|
+
aria-label={t('Enable')}
|
|
222
|
+
key="enable"
|
|
223
|
+
size="small"
|
|
224
|
+
disabled={builtIn || error}
|
|
225
|
+
onChange={(checked, e) => {
|
|
226
|
+
e.stopPropagation();
|
|
227
|
+
handleSwitchChange(checked);
|
|
228
|
+
}}
|
|
229
|
+
checked={!!enabled}
|
|
230
|
+
/>
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
return (
|
|
234
|
+
<>
|
|
235
|
+
{detailOpen && <PluginDetail plugin={data} onCancel={() => setDetailOpen(false)} />}
|
|
236
|
+
<Card
|
|
237
|
+
role="button"
|
|
238
|
+
aria-label={title}
|
|
239
|
+
size="small"
|
|
240
|
+
variant="borderless"
|
|
241
|
+
hoverable
|
|
242
|
+
onClick={openDetail}
|
|
243
|
+
styles={{
|
|
244
|
+
body: { paddingTop: token.paddingSM },
|
|
245
|
+
header: { border: 'none', minHeight: 'inherit', paddingTop: token.padding },
|
|
246
|
+
}}
|
|
247
|
+
className={cardClassName}
|
|
248
|
+
title={cardTitle}
|
|
249
|
+
actions={[linksAction, switchAction]}
|
|
250
|
+
>
|
|
251
|
+
<Card.Meta
|
|
252
|
+
description={
|
|
253
|
+
<Typography.Paragraph
|
|
254
|
+
type="secondary"
|
|
255
|
+
className={css`
|
|
256
|
+
display: -webkit-box;
|
|
257
|
+
-webkit-box-orient: vertical;
|
|
258
|
+
-webkit-line-clamp: 2;
|
|
259
|
+
overflow: hidden;
|
|
260
|
+
margin-bottom: 0;
|
|
261
|
+
`}
|
|
262
|
+
>
|
|
263
|
+
{description}
|
|
264
|
+
</Typography.Paragraph>
|
|
265
|
+
}
|
|
266
|
+
/>
|
|
267
|
+
</Card>
|
|
268
|
+
</>
|
|
269
|
+
);
|
|
270
|
+
};
|