@nocobase/client-v2 2.1.0-alpha.40 → 2.1.0-alpha.45

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.
Files changed (278) hide show
  1. package/es/Application.d.ts +7 -0
  2. package/es/BaseApplication.d.ts +13 -0
  3. package/es/RouterManager.d.ts +1 -0
  4. package/es/collection-field-interface/CollectionFieldInterface.d.ts +51 -15
  5. package/es/collection-field-interface/CollectionFieldInterfaceManager.d.ts +82 -3
  6. package/es/collection-manager/field-configure.d.ts +80 -0
  7. package/es/collection-manager/field-validation.d.ts +43 -0
  8. package/es/collection-manager/filter-operators/index.d.ts +46 -0
  9. package/es/collection-manager/filter-operators/operators.d.ts +30 -0
  10. package/es/collection-manager/interfaces/checkbox.d.ts +1 -41
  11. package/es/collection-manager/interfaces/checkboxGroup.d.ts +12 -44
  12. package/es/collection-manager/interfaces/collection.d.ts +12 -51
  13. package/es/collection-manager/interfaces/color.d.ts +1 -16
  14. package/es/collection-manager/interfaces/createdAt.d.ts +1 -44
  15. package/es/collection-manager/interfaces/createdBy.d.ts +0 -4
  16. package/es/collection-manager/interfaces/dateOnly.d.ts +7 -44
  17. package/es/collection-manager/interfaces/datetime.d.ts +1 -44
  18. package/es/collection-manager/interfaces/datetimeNoTz.d.ts +1 -44
  19. package/es/collection-manager/interfaces/email.d.ts +1 -29
  20. package/es/collection-manager/interfaces/id.d.ts +1 -16
  21. package/es/collection-manager/interfaces/index.d.ts +2 -3
  22. package/es/collection-manager/interfaces/input.d.ts +1 -102
  23. package/es/collection-manager/interfaces/integer.d.ts +1 -95
  24. package/es/collection-manager/interfaces/json.d.ts +16 -7
  25. package/es/collection-manager/interfaces/m2m.d.ts +11 -19
  26. package/es/collection-manager/interfaces/m2o.d.ts +11 -19
  27. package/es/collection-manager/interfaces/markdown.d.ts +1 -63
  28. package/es/collection-manager/interfaces/multipleSelect.d.ts +12 -44
  29. package/es/collection-manager/interfaces/nanoid.d.ts +1 -34
  30. package/es/collection-manager/interfaces/number.d.ts +1 -87
  31. package/es/collection-manager/interfaces/o2m.d.ts +12 -24
  32. package/es/collection-manager/interfaces/obo.d.ts +207 -0
  33. package/es/collection-manager/interfaces/oho.d.ts +207 -0
  34. package/es/collection-manager/interfaces/password.d.ts +1 -56
  35. package/es/collection-manager/interfaces/percent.d.ts +1 -84
  36. package/es/collection-manager/interfaces/phone.d.ts +1 -25
  37. package/es/collection-manager/interfaces/properties/index.d.ts +0 -28
  38. package/es/collection-manager/interfaces/radioGroup.d.ts +1 -29
  39. package/es/collection-manager/interfaces/richText.d.ts +1 -63
  40. package/es/collection-manager/interfaces/select.d.ts +12 -44
  41. package/es/collection-manager/interfaces/snowflake-id.d.ts +1 -34
  42. package/es/collection-manager/interfaces/tableoid.d.ts +1 -10
  43. package/es/collection-manager/interfaces/textarea.d.ts +1 -51
  44. package/es/collection-manager/interfaces/time.d.ts +1 -16
  45. package/es/collection-manager/interfaces/types.d.ts +3 -12
  46. package/es/collection-manager/interfaces/unixTimestamp.d.ts +1 -44
  47. package/es/collection-manager/interfaces/updatedAt.d.ts +1 -44
  48. package/es/collection-manager/interfaces/updatedBy.d.ts +0 -4
  49. package/es/collection-manager/interfaces/url.d.ts +1 -20
  50. package/es/collection-manager/interfaces/uuid.d.ts +1 -34
  51. package/es/collection-manager/template-fields.d.ts +53 -0
  52. package/es/components/KeepAlive.d.ts +22 -0
  53. package/es/components/RouterBridge.d.ts +9 -0
  54. package/es/components/form/DialogFormLayout.d.ts +5 -29
  55. package/es/components/form/VariableInput.d.ts +53 -2
  56. package/es/components/form/filter/CollectionFilter.d.ts +49 -0
  57. package/es/components/form/filter/CollectionFilterItem.d.ts +49 -0
  58. package/es/components/form/filter/DateFilterDynamicComponent.d.ts +57 -0
  59. package/es/components/form/filter/FilterValueInput.d.ts +29 -0
  60. package/es/components/form/filter/index.d.ts +11 -0
  61. package/es/components/form/filter/useFilterActionProps.d.ts +96 -0
  62. package/es/components/form/index.d.ts +1 -0
  63. package/es/data-source/ExtendCollectionsProvider.d.ts +50 -0
  64. package/es/data-source/index.d.ts +9 -0
  65. package/es/flow/FlowPage.d.ts +2 -1
  66. package/es/flow/admin-shell/AdminLayoutRouteCoordinator.d.ts +8 -40
  67. package/es/flow/admin-shell/BaseLayoutModel.d.ts +89 -0
  68. package/es/flow/admin-shell/BaseLayoutRouteCoordinator.d.ts +74 -0
  69. package/es/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.d.ts +12 -0
  70. package/es/flow/admin-shell/admin-layout/AdminLayoutModel.d.ts +7 -92
  71. package/es/flow/admin-shell/admin-layout/AppListRender.d.ts +11 -0
  72. package/es/flow/admin-shell/admin-layout/index.d.ts +3 -0
  73. package/es/flow/admin-shell/admin-layout/useApplications.d.ts +3 -2
  74. package/es/flow/admin-shell/useAdminLayoutRoutePage.d.ts +2 -2
  75. package/es/flow/admin-shell/useLayoutRoutePage.d.ts +23 -0
  76. package/es/flow/components/FlowRoute.d.ts +10 -1
  77. package/es/flow/components/filter/index.d.ts +2 -0
  78. package/es/flow/components/filter/useFilterOptions.d.ts +66 -0
  79. package/es/flow/index.d.ts +4 -0
  80. package/es/flow/models/base/PageModel/PageModel.d.ts +3 -1
  81. package/es/flow/models/blocks/assign-form/assignFieldValuesFlow.d.ts +84 -0
  82. package/es/flow/models/blocks/assign-form/index.d.ts +1 -0
  83. package/es/flow/models/blocks/form/FormActionGroupModel.d.ts +1 -0
  84. package/es/flow/models/blocks/form/FormActionModel.d.ts +9 -2
  85. package/es/flow/models/blocks/table/TableBlockModel.d.ts +10 -0
  86. package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.d.ts +1 -1
  87. package/es/flow-compat/passwordUtils.d.ts +1 -1
  88. package/es/index.d.ts +6 -0
  89. package/es/index.mjs +552 -459
  90. package/es/layout-manager/LayoutContentRoute.d.ts +14 -0
  91. package/es/layout-manager/LayoutManager.d.ts +22 -0
  92. package/es/layout-manager/LayoutRoute.d.ts +14 -0
  93. package/es/layout-manager/index.d.ts +13 -0
  94. package/es/layout-manager/types.d.ts +20 -0
  95. package/es/layout-manager/utils.d.ts +14 -0
  96. package/es/nocobase-buildin-plugin/index.d.ts +3 -10
  97. package/es/settings-center/index.d.ts +1 -1
  98. package/es/settings-center/plugin-manager/BulkEnableButton.d.ts +15 -0
  99. package/es/settings-center/plugin-manager/PluginCard.d.ts +15 -0
  100. package/es/settings-center/plugin-manager/PluginDetail.d.ts +16 -0
  101. package/es/settings-center/{PluginManagerPage.d.ts → plugin-manager/index.d.ts} +1 -7
  102. package/es/settings-center/plugin-manager/types.d.ts +34 -0
  103. package/lib/index.js +552 -459
  104. package/package.json +8 -7
  105. package/src/Application.tsx +51 -12
  106. package/src/BaseApplication.tsx +32 -0
  107. package/src/PluginSettingsManager.ts +1 -1
  108. package/src/RouterManager.tsx +17 -1
  109. package/src/__tests__/PluginSettingsManager.test.ts +41 -2
  110. package/src/__tests__/app.test.tsx +17 -1
  111. package/src/__tests__/globalDeps.test.ts +1 -0
  112. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +45 -2
  113. package/src/__tests__/plugin-manager.test.tsx +177 -0
  114. package/src/__tests__/settings-center.test.tsx +24 -2
  115. package/src/collection-field-interface/CollectionFieldInterface.ts +71 -77
  116. package/src/collection-field-interface/CollectionFieldInterfaceManager.ts +201 -4
  117. package/src/collection-manager/field-configure.ts +548 -0
  118. package/src/collection-manager/field-validation.ts +195 -0
  119. package/src/collection-manager/filter-operators/index.ts +176 -0
  120. package/src/collection-manager/{interfaces/properties → filter-operators}/operators.ts +24 -13
  121. package/src/collection-manager/interfaces/checkbox.ts +2 -9
  122. package/src/collection-manager/interfaces/checkboxGroup.ts +2 -10
  123. package/src/collection-manager/interfaces/collection.ts +2 -15
  124. package/src/collection-manager/interfaces/color.ts +2 -2
  125. package/src/collection-manager/interfaces/createdAt.ts +2 -2
  126. package/src/collection-manager/interfaces/createdBy.ts +1 -12
  127. package/src/collection-manager/interfaces/dateOnly.ts +8 -2
  128. package/src/collection-manager/interfaces/datetime.ts +2 -2
  129. package/src/collection-manager/interfaces/datetimeNoTz.ts +2 -2
  130. package/src/collection-manager/interfaces/email.ts +2 -9
  131. package/src/collection-manager/interfaces/id.ts +1 -2
  132. package/src/collection-manager/interfaces/index.ts +2 -3
  133. package/src/collection-manager/interfaces/input.ts +2 -133
  134. package/src/collection-manager/interfaces/integer.ts +2 -71
  135. package/src/collection-manager/interfaces/json.tsx +17 -11
  136. package/src/collection-manager/interfaces/m2m.tsx +0 -21
  137. package/src/collection-manager/interfaces/m2o.tsx +0 -22
  138. package/src/collection-manager/interfaces/markdown.ts +2 -51
  139. package/src/collection-manager/interfaces/multipleSelect.ts +2 -14
  140. package/src/collection-manager/interfaces/nanoid.ts +2 -2
  141. package/src/collection-manager/interfaces/number.ts +2 -85
  142. package/src/collection-manager/interfaces/o2m.tsx +1 -22
  143. package/src/collection-manager/interfaces/obo.tsx +145 -0
  144. package/src/collection-manager/interfaces/oho.tsx +145 -0
  145. package/src/collection-manager/interfaces/password.ts +2 -44
  146. package/src/collection-manager/interfaces/percent.ts +2 -74
  147. package/src/collection-manager/interfaces/phone.ts +2 -2
  148. package/src/collection-manager/interfaces/properties/index.ts +0 -133
  149. package/src/collection-manager/interfaces/radioGroup.ts +2 -2
  150. package/src/collection-manager/interfaces/richText.ts +2 -51
  151. package/src/collection-manager/interfaces/select.ts +2 -14
  152. package/src/collection-manager/interfaces/snowflake-id.ts +2 -2
  153. package/src/collection-manager/interfaces/tableoid.ts +1 -2
  154. package/src/collection-manager/interfaces/textarea.ts +2 -51
  155. package/src/collection-manager/interfaces/time.ts +2 -2
  156. package/src/collection-manager/interfaces/types.ts +4 -12
  157. package/src/collection-manager/interfaces/unixTimestamp.tsx +2 -2
  158. package/src/collection-manager/interfaces/updatedAt.ts +2 -2
  159. package/src/collection-manager/interfaces/updatedBy.ts +1 -12
  160. package/src/collection-manager/interfaces/url.ts +2 -4
  161. package/src/collection-manager/interfaces/uuid.ts +2 -2
  162. package/src/collection-manager/template-fields.ts +109 -0
  163. package/src/components/KeepAlive.tsx +131 -0
  164. package/src/components/README.md +90 -6
  165. package/src/components/README.zh-CN.md +90 -7
  166. package/src/components/RouterBridge.tsx +28 -4
  167. package/src/components/__tests__/KeepAlive.test.tsx +63 -0
  168. package/src/components/__tests__/RouterBridge.test.tsx +27 -0
  169. package/src/components/form/DialogFormLayout.tsx +5 -29
  170. package/src/components/form/VariableInput.tsx +101 -28
  171. package/src/components/form/__tests__/VariableInput.test.ts +85 -0
  172. package/src/components/form/filter/CollectionFilter.tsx +111 -0
  173. package/src/components/form/filter/CollectionFilterItem.tsx +184 -0
  174. package/src/components/form/filter/DateFilterDynamicComponent.tsx +283 -0
  175. package/src/components/form/filter/FilterValueInput.tsx +198 -0
  176. package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +247 -0
  177. package/src/components/form/filter/__tests__/DateFilterDynamicComponent.test.tsx +148 -0
  178. package/src/components/form/filter/__tests__/FilterValueInput.test.tsx +243 -0
  179. package/src/components/form/filter/__tests__/compileFilterGroup.test.ts +146 -0
  180. package/src/components/form/filter/index.ts +13 -0
  181. package/src/components/form/filter/useFilterActionProps.ts +203 -0
  182. package/src/components/form/index.tsx +1 -0
  183. package/src/data-source/ExtendCollectionsProvider.tsx +144 -0
  184. package/src/data-source/__tests__/ExtendCollectionsProvider.test.tsx +264 -0
  185. package/src/data-source/index.ts +10 -0
  186. package/src/flow/FlowPage.tsx +35 -7
  187. package/src/flow/__tests__/FlowPage.test.tsx +79 -0
  188. package/src/flow/__tests__/FlowRoute.test.tsx +529 -2
  189. package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +191 -0
  190. package/src/flow/actions/__tests__/openView.subModelKey.test.tsx +33 -0
  191. package/src/flow/actions/aclCheck.tsx +4 -0
  192. package/src/flow/actions/aclCheckRefresh.tsx +4 -0
  193. package/src/flow/actions/dateTimeFormat.tsx +12 -8
  194. package/src/flow/actions/linkageRules.tsx +122 -0
  195. package/src/flow/actions/openView.tsx +28 -4
  196. package/src/flow/admin-shell/AdminLayoutRouteCoordinator.ts +11 -329
  197. package/src/flow/admin-shell/BaseLayoutModel.tsx +455 -0
  198. package/src/flow/admin-shell/BaseLayoutRouteCoordinator.ts +502 -0
  199. package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +547 -3
  200. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +35 -7
  201. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +160 -0
  202. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +0 -12
  203. package/src/flow/admin-shell/admin-layout/AdminLayoutModel.tsx +28 -201
  204. package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +11 -2
  205. package/src/flow/admin-shell/admin-layout/AppListRender.tsx +139 -0
  206. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +1 -26
  207. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +149 -27
  208. package/src/flow/admin-shell/admin-layout/index.ts +3 -0
  209. package/src/flow/admin-shell/admin-layout/useApplications.tsx +34 -1
  210. package/src/flow/admin-shell/useAdminLayoutRoutePage.ts +10 -26
  211. package/src/flow/admin-shell/useLayoutRoutePage.ts +61 -0
  212. package/src/flow/components/AdminLayout.tsx +4 -154
  213. package/src/flow/components/FlowRoute.tsx +105 -15
  214. package/src/flow/components/filter/index.ts +3 -0
  215. package/src/flow/components/filter/useFilterOptions.ts +102 -0
  216. package/src/flow/index.ts +4 -0
  217. package/src/flow/models/actions/UpdateRecordActionModel.tsx +14 -95
  218. package/src/flow/models/actions/UpdateRecordActionUtils.ts +4 -7
  219. package/src/flow/models/actions/__tests__/AssignFormRefill.test.ts +26 -1
  220. package/src/flow/models/base/ActionModel.tsx +8 -1
  221. package/src/flow/models/base/PageModel/PageModel.tsx +51 -18
  222. package/src/flow/models/base/PageModel/RootPageModel.tsx +6 -13
  223. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +102 -1
  224. package/src/flow/models/base/RouteModel.tsx +1 -1
  225. package/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx +63 -2
  226. package/src/flow/models/blocks/assign-form/assignFieldValuesFlow.tsx +206 -0
  227. package/src/flow/models/blocks/assign-form/index.ts +1 -0
  228. package/src/flow/models/blocks/form/FormActionGroupModel.tsx +14 -0
  229. package/src/flow/models/blocks/form/FormActionModel.tsx +30 -3
  230. package/src/flow/models/blocks/form/FormItemModel.tsx +8 -1
  231. package/src/flow/models/blocks/form/__tests__/FormActionGroupModel.test.ts +46 -0
  232. package/src/flow/models/blocks/form/__tests__/submitHandler.test.ts +71 -0
  233. package/src/flow/models/blocks/form/submitHandler.ts +8 -1
  234. package/src/flow/models/blocks/form/submitValues.ts +4 -1
  235. package/src/flow/models/blocks/table/TableBlockModel.tsx +118 -16
  236. package/src/flow/models/blocks/table/__tests__/TableBlockModel.rowSelection.test.tsx +114 -0
  237. package/src/flow/models/fields/AssociationFieldModel/SubFormFieldModel.tsx +7 -1
  238. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +1 -1
  239. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +6 -5
  240. package/src/flow/models/fields/ClickableFieldModel.tsx +9 -1
  241. package/src/flow/models/fields/CollectionSelectorFieldModel.tsx +8 -2
  242. package/src/flow/models/fields/DisplayEnumFieldModel.tsx +8 -2
  243. package/src/flow/models/fields/DisplayTimeFieldModel.tsx +1 -1
  244. package/src/flow/models/fields/TimeFieldModel.tsx +1 -1
  245. package/src/flow/models/fields/__tests__/TimeFieldModel.test.tsx +61 -0
  246. package/src/flow/models/fields/mobile-components/MobileDatePicker.tsx +19 -3
  247. package/src/flow/models/fields/mobile-components/__tests__/MobileDatePicker.test.tsx +94 -0
  248. package/src/flow/models/topbar/TopbarActionModel.tsx +1 -1
  249. package/src/flow/utils/__tests__/dateTimeFormat.test.ts +91 -0
  250. package/src/index.ts +6 -0
  251. package/src/layout-manager/LayoutContentRoute.tsx +90 -0
  252. package/src/layout-manager/LayoutManager.tsx +185 -0
  253. package/src/layout-manager/LayoutRoute.tsx +138 -0
  254. package/src/layout-manager/__tests__/LayoutManager.test.tsx +335 -0
  255. package/src/layout-manager/__tests__/LayoutRoute.test.tsx +473 -0
  256. package/src/layout-manager/index.ts +14 -0
  257. package/src/layout-manager/types.ts +22 -0
  258. package/src/layout-manager/utils.ts +37 -0
  259. package/src/nocobase-buildin-plugin/index.tsx +69 -67
  260. package/src/nocobase-buildin-plugin/plugins/LocalePlugin.ts +1 -0
  261. package/src/settings-center/index.ts +1 -1
  262. package/src/settings-center/plugin-manager/BulkEnableButton.tsx +111 -0
  263. package/src/settings-center/plugin-manager/PluginCard.tsx +270 -0
  264. package/src/settings-center/plugin-manager/PluginDetail.tsx +195 -0
  265. package/src/settings-center/plugin-manager/index.tsx +254 -0
  266. package/src/settings-center/plugin-manager/types.ts +35 -0
  267. package/src/settings-center/utils.tsx +8 -1
  268. package/src/theme/__tests__/globalStyles.test.ts +24 -0
  269. package/src/theme/globalStyles.ts +10 -0
  270. package/src/utils/globalDeps.ts +2 -0
  271. package/es/collection-manager/interfaces/linkTo.d.ts +0 -90
  272. package/es/collection-manager/interfaces/o2o.d.ts +0 -621
  273. package/es/collection-manager/interfaces/properties/operators.d.ts +0 -294
  274. package/es/collection-manager/interfaces/subTable.d.ts +0 -172
  275. package/src/collection-manager/interfaces/linkTo.ts +0 -120
  276. package/src/collection-manager/interfaces/o2o.tsx +0 -561
  277. package/src/collection-manager/interfaces/subTable.ts +0 -218
  278. 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
 
@@ -64,15 +88,9 @@ export function useCurrentUserContext() {
64
88
  }
65
89
 
66
90
  /**
67
- * 返回当前用户在 v2 应用上下文中可选的角色列表,等价于 v1 `useCurrentRoles`:
68
- * 从 FlowEngine 全局上下文 `engine.context.user.roles` 派生(CurrentUserProvider 在
69
- * `/auth:check` 成功后通过 `defineProperty('user', { value })` 写入),按需追加匿名角色,
70
- * 并去掉合并角色 `__union__`。v2 中角色 title 可能含有 `{{t('...')}}` 模板,因此用
71
- * flowEngine.context.t 解析。
91
+ * 返回当前用户在 v2 应用上下文中可选的角色列表,等价于 v1 `useCurrentRoles`:从 FlowEngine 全局上下文 `engine.context.user.roles` 派生(CurrentUserProvider 在 `/auth:check` 成功后通过 `defineProperty('user', { value })` 写入),按需追加匿名角色,并去掉合并角色 `__union__`。v2 中角色 title 可能含有 `{{t('...')}}` 模板,因此用 flowEngine.context.t 解析。
72
92
  *
73
- * 不读 React `CurrentUserContext`:FlowEngine 的 dialog/drawer/popover 内容通过 `ctx.viewer`
74
- * 渲染到独立的 ElementsHolder,部分场景会脱离原 Provider 树;FlowEngine 全局上下文是同一份
75
- * 数据但不受 React 树位置影响。
93
+ * 不读 React `CurrentUserContext`:FlowEngine 的 dialog/drawer/popover 内容通过 `ctx.viewer` 渲染到独立的 ElementsHolder,部分场景会脱离原 Provider 树;FlowEngine 全局上下文是同一份数据但不受 React 树位置影响。
76
94
  */
77
95
  export function useCurrentRoles(): CurrentRoleOption[] {
78
96
  const { allowAnonymous } = useACLRoleContext();
@@ -93,7 +111,7 @@ export function useCurrentRoles(): CurrentRoleOption[] {
93
111
  }
94
112
 
95
113
  const DataSourceBootstrapProvider: FC = ({ children }) => {
96
- const app = useApp();
114
+ const app = useApp<Application>();
97
115
  const location = useLocation();
98
116
  const [loading, setLoading] = useState(true);
99
117
  const [error, setError] = useState<Error | null>(null);
@@ -104,7 +122,7 @@ const DataSourceBootstrapProvider: FC = ({ children }) => {
104
122
  const basename = app.router.getBasename();
105
123
  const isSkippedAuthCheckRoute =
106
124
  isBuiltinAuthRoute(location.pathname, basename) || app.router.isSkippedAuthCheckRoute(location.pathname);
107
- const shouldBootstrap = isAdminRuntimeRoute(location.pathname, basename);
125
+ const shouldBootstrap = shouldCheckRuntimeRoute(app, location.pathname);
108
126
 
109
127
  if (isSkippedAuthCheckRoute || !shouldBootstrap) {
110
128
  setLoading(false);
@@ -152,28 +170,25 @@ const DataSourceBootstrapProvider: FC = ({ children }) => {
152
170
  };
153
171
 
154
172
  const CurrentUserProvider: FC = ({ children }) => {
155
- const app = useApp();
173
+ const app = useApp<Application>();
156
174
  const location = useLocation();
157
175
  const navigate = useNavigate();
158
176
  const [state, setState] = useState<CurrentUserState>({ loading: true });
159
- const pathnameRef = useRef(location.pathname);
160
- pathnameRef.current = location.pathname;
161
177
  const locationRef = useRef(location);
162
178
  locationRef.current = location;
179
+ const authCheckRouteState = getCurrentUserAuthCheckRouteState(app, location.pathname);
163
180
 
164
181
  useEffect(() => {
165
182
  let mounted = true;
166
- const isSkippedAuthCheckRoute =
167
- isBuiltinAuthRoute(pathnameRef.current, app.router.getBasename()) ||
168
- app.router.isSkippedAuthCheckRoute(pathnameRef.current);
169
- const shouldCheckCurrentUser = isAdminRuntimeRoute(pathnameRef.current, app.router.getBasename());
170
183
 
171
- if (isSkippedAuthCheckRoute || !shouldCheckCurrentUser) {
184
+ if (authCheckRouteState !== 'required') {
172
185
  // 认证页等免鉴权路由不应再执行 `/auth:check`,否则未登录时会重复鉴权并触发重定向抖动。
173
186
  setState({ loading: false });
174
187
  return;
175
188
  }
176
189
 
190
+ setState((previous) => (previous.loading ? previous : { ...previous, loading: true }));
191
+
177
192
  const run = async () => {
178
193
  try {
179
194
  const res = await app.apiClient.request({
@@ -182,21 +197,18 @@ const CurrentUserProvider: FC = ({ children }) => {
182
197
  skipAuth: true,
183
198
  });
184
199
 
200
+ if (!mounted) {
201
+ return;
202
+ }
203
+
185
204
  const user = res?.data?.data;
186
- // 服务端通过 `{ code: 302, redirect }` 通知客户端先去某个中间页(例如 2FA 验证页)
187
- // 这类响应没有 user.id,但也不能视为未登录——否则会和处理 302 的全局响应拦截器
188
- // (例如 plugin-two-factor-authentication 注册的那一个)竞态,而 `window.location.replace`
189
- // 会覆盖更早发出的 `window.location.href`,把用户错误地弹回登录页。让响应拦截器接管跳转。
205
+ // 服务端通过 `{ code: 302, redirect }` 通知客户端先去某个中间页(例如 2FA 验证页)。这类响应没有 user.id,但也不能视为未登录——否则会和处理 302 的全局响应拦截器 (例如 plugin-two-factor-authentication 注册的那一个)竞态,而 `window.location.replace` 会覆盖更早发出的 `window.location.href`,把用户错误地弹回登录页。让响应拦截器接管跳转。
190
206
  if (user?.code === 302) {
191
- if (mounted) {
192
- setState({ loading: false });
193
- }
207
+ setState({ loading: false });
194
208
  return;
195
209
  }
196
210
  if (user?.id == null) {
197
- // 用 react-router navigate (虚拟跳转)而不是 location.replace, 这样如果有其他响应拦截器
198
- // 已经发起了 window.location.href 整页跳转(例如 2FA 插件接收到服务端 302 重定向),
199
- // 真实跳转可以胜出 navigate, 不会被这里的 signin 重定向覆盖。
211
+ // 用 react-router navigate (虚拟跳转)而不是 location.replace, 这样如果有其他响应拦截器已经发起了 window.location.href 整页跳转(例如 2FA 插件接收到服务端 302 重定向), 真实跳转可以胜出 navigate, 不会被这里的 signin 重定向覆盖。
200
212
  navigate(`/signin?redirect=${encodeURIComponent(getCurrentV2RedirectPath(app, locationRef.current))}`, {
201
213
  replace: true,
202
214
  });
@@ -215,13 +227,15 @@ const CurrentUserProvider: FC = ({ children }) => {
215
227
  meta: userMeta,
216
228
  });
217
229
 
218
- if (mounted) {
219
- setState({
220
- data: res?.data,
221
- loading: false,
222
- });
223
- }
230
+ setState({
231
+ data: res?.data,
232
+ loading: false,
233
+ });
224
234
  } catch (error: any) {
235
+ if (!mounted) {
236
+ return;
237
+ }
238
+
225
239
  const isAuthError = error?.response?.status === 401 || error?.status === 401;
226
240
  if (isAuthError) {
227
241
  navigate(`/signin?redirect=${encodeURIComponent(getCurrentV2RedirectPath(app, locationRef.current))}`, {
@@ -229,19 +243,17 @@ const CurrentUserProvider: FC = ({ children }) => {
229
243
  });
230
244
  return;
231
245
  }
232
- if (mounted) {
233
- setState({ loading: false });
234
- }
246
+ setState({ loading: false });
235
247
  throw error;
236
248
  }
237
249
  };
238
250
 
239
- void run();
251
+ run();
240
252
 
241
253
  return () => {
242
254
  mounted = false;
243
255
  };
244
- }, [app, navigate]);
256
+ }, [app, authCheckRouteState, navigate]);
245
257
 
246
258
  if (state.loading) {
247
259
  return app.renderComponent('AppSpin');
@@ -251,13 +263,12 @@ const CurrentUserProvider: FC = ({ children }) => {
251
263
  };
252
264
 
253
265
  const RootRedirect: FC = () => {
254
- const app = useApp();
266
+ const app = useApp<Application>();
255
267
  const hasToken = !!app?.apiClient?.auth?.token;
256
268
  const targetPath = getDefaultV2AdminRedirectPath(app);
257
269
 
258
270
  if (!hasToken) {
259
- // 用 react-router <Navigate /> 而非 location.replace, 避免覆盖同时段其它响应拦截器
260
- // 触发的 window.location.href (例如 2FA 接收到服务端 302 时设置的整页跳转)。
271
+ // 用 react-router <Navigate /> 而非 location.replace, 避免覆盖同时段其它响应拦截器触发的 window.location.href (例如 2FA 接收到服务端 302 时设置的整页跳转)。
261
272
  return <Navigate replace to={`/signin?redirect=${encodeURIComponent(targetPath)}`} />;
262
273
  }
263
274
 
@@ -267,8 +278,7 @@ const RootRedirect: FC = () => {
267
278
  /**
268
279
  * client-v2 使用的内建插件集合。
269
280
  *
270
- * 只迁移当前 v2 运行时仍然需要的部分,显式跳过 schemaInitializerManager
271
- * 以及用户标注暂不迁移的旧插件注册逻辑。
281
+ * 只迁移当前 v2 运行时仍然需要的部分,显式跳过 schemaInitializerManager 以及用户标注暂不迁移的旧插件注册逻辑。
272
282
  */
273
283
  export class NocoBaseBuildInPlugin extends Plugin<any, Application> {
274
284
  async afterAdd() {
@@ -286,6 +296,12 @@ export class NocoBaseBuildInPlugin extends Plugin<any, Application> {
286
296
  AdminLayoutMenuItemModel,
287
297
  AdminSettingsLayoutModel,
288
298
  });
299
+ this.app.layoutManager.registerLayout({
300
+ routeName: 'admin',
301
+ routePath: '/admin',
302
+ uid: ADMIN_LAYOUT_MODEL_UID,
303
+ layoutModelClass: 'AdminLayoutModel',
304
+ });
289
305
 
290
306
  this.app.pluginSettingsManager.addMenuItem({
291
307
  key: 'plugin-manager',
@@ -298,7 +314,7 @@ export class NocoBaseBuildInPlugin extends Plugin<any, Application> {
298
314
  menuKey: 'plugin-manager',
299
315
  key: 'index',
300
316
  title: this.app.i18n.t('Plugin manager'),
301
- componentLoader: () => import('../settings-center/PluginManagerPage'),
317
+ componentLoader: () => import('../settings-center/plugin-manager'),
302
318
  aclSnippet: 'pm',
303
319
  sort: -200,
304
320
  });
@@ -317,6 +333,13 @@ export class NocoBaseBuildInPlugin extends Plugin<any, Application> {
317
333
  aclSnippet: 'pm.system-settings.system-settings',
318
334
  sort: -100,
319
335
  });
336
+ // Parent menu for security-related plugin settings (password policy, locked users, etc.). Registered here in the buildin plugin so any pro plugin can attach page tabs to `menuKey: 'security'` without each one re-registering the same parent.
337
+ this.app.pluginSettingsManager.addMenuItem({
338
+ key: 'security',
339
+ title: this.app.i18n.t('Security'),
340
+ icon: 'SafetyOutlined',
341
+ aclSnippet: 'pm.security',
342
+ });
320
343
  }
321
344
 
322
345
  addRoutes() {
@@ -330,10 +353,6 @@ export class NocoBaseBuildInPlugin extends Plugin<any, Application> {
330
353
  Component: AppNotFound,
331
354
  });
332
355
 
333
- this.router.add('admin', {
334
- path: '/admin',
335
- componentLoader: () => import('../flow/components/AdminLayout'),
336
- });
337
356
  this.router.add('admin.settings', {
338
357
  path: '/admin/settings',
339
358
  componentLoader: () => import('../settings-center/AdminSettingsLayout'),
@@ -342,23 +361,6 @@ export class NocoBaseBuildInPlugin extends Plugin<any, Application> {
342
361
  path: '*',
343
362
  Component: Outlet,
344
363
  });
345
- this.router.add('admin.page', {
346
- path: '/admin/:name',
347
- componentLoader: () => import('../flow/components/FlowRoute'),
348
- });
349
-
350
- this.router.add('admin.page.tab', {
351
- path: '/admin/:name/tab/:tabUid',
352
- componentLoader: () => import('../flow/components/FlowRoute'),
353
- });
354
- this.router.add('admin.page.view', {
355
- path: '/admin/:name/view/*',
356
- componentLoader: () => import('../flow/components/FlowRoute'),
357
- });
358
- this.router.add('admin.page.tab.view', {
359
- path: '/admin/:name/tab/:tabUid/view/*',
360
- componentLoader: () => import('../flow/components/FlowRoute'),
361
- });
362
364
  }
363
365
 
364
366
  addComponents() {}
@@ -39,6 +39,7 @@ export class LocalePlugin extends Plugin {
39
39
 
40
40
  if (data.lang) {
41
41
  api.auth.setLocale(data.lang);
42
+ this.app.setDocumentLanguage(data.lang);
42
43
  this.app.i18n.changeLanguage(data.lang);
43
44
  }
44
45
 
@@ -9,6 +9,6 @@
9
9
 
10
10
  export * from './AdminSettingsLayout';
11
11
  export * from './AdminSettingsLayoutModel';
12
- export * from './PluginManagerPage';
12
+ export * from './plugin-manager';
13
13
  export * from './SystemSettingsPage';
14
14
  export * from './utils';
@@ -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
+ };