@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nocobase/client-v2",
3
- "version": "2.1.0-alpha.40",
3
+ "version": "2.1.0-alpha.45",
4
4
  "license": "Apache-2.0",
5
5
  "main": "lib/index.js",
6
6
  "module": "es/index.mjs",
@@ -20,17 +20,18 @@
20
20
  },
21
21
  "dependencies": {
22
22
  "@ant-design/icons": "^5.6.1",
23
+ "@ctrl/tinycolor": "^3.6.0",
23
24
  "@dnd-kit/core": "^6.0.0",
24
25
  "@dnd-kit/sortable": "^7.0.0",
25
26
  "@emotion/css": "^11.7.1",
26
27
  "@formily/antd-v5": "1.2.3",
27
28
  "@formily/react": "^2.2.27",
28
29
  "@formily/shared": "^2.2.27",
29
- "@nocobase/evaluators": "2.1.0-alpha.40",
30
- "@nocobase/flow-engine": "2.1.0-alpha.40",
31
- "@nocobase/sdk": "2.1.0-alpha.40",
32
- "@nocobase/shared": "2.1.0-alpha.40",
33
- "@nocobase/utils": "2.1.0-alpha.40",
30
+ "@nocobase/evaluators": "2.1.0-alpha.45",
31
+ "@nocobase/flow-engine": "2.1.0-alpha.45",
32
+ "@nocobase/sdk": "2.1.0-alpha.45",
33
+ "@nocobase/shared": "2.1.0-alpha.45",
34
+ "@nocobase/utils": "2.1.0-alpha.45",
34
35
  "ahooks": "^3.7.2",
35
36
  "antd": "5.24.2",
36
37
  "antd-style": "3.7.1",
@@ -44,5 +45,5 @@
44
45
  "react-i18next": "^11.15.1",
45
46
  "react-router-dom": "^6.30.1"
46
47
  },
47
- "gitHead": "e73f99dd0abefe847f2e50ff0fea1f41a82fd048"
48
+ "gitHead": "e9e24987e12d0ad10a5db8815b1e1b7b447f1938"
48
49
  }
@@ -42,6 +42,7 @@ export class Application extends BaseApplication<
42
42
  PluginSettingsManager
43
43
  > {
44
44
  public declare dataSourceManager: any;
45
+ public hasLoadError = false;
45
46
 
46
47
  protected createApiClient(options: ApplicationOptions) {
47
48
  return new APIClient({
@@ -99,9 +100,29 @@ export class Application extends BaseApplication<
99
100
  }
100
101
 
101
102
  async load() {
102
- await this.loadWebSocket();
103
- await this.pm.load();
104
- await this.flowEngine.flowSettings.load();
103
+ try {
104
+ this.hasLoadError = false;
105
+ await this.loadWebSocket();
106
+ await this.pm.load();
107
+ await this.flowEngine.flowSettings.load();
108
+ } catch (error: any) {
109
+ this.hasLoadError = true;
110
+
111
+ if (error?.response?.data?.errors?.[0]?.code === 'BLOCKED_IP') {
112
+ this.hasLoadError = false;
113
+ }
114
+
115
+ if (this.ws.enabled) {
116
+ await new Promise((resolve) => {
117
+ setTimeout(() => resolve(null), 1000);
118
+ });
119
+ }
120
+ this.error = {
121
+ code: 'LOAD_ERROR',
122
+ ...this.apiClient.toErrMessages(error)?.[0],
123
+ };
124
+ console.error(error, this.error);
125
+ }
105
126
  this.updateFavicon();
106
127
  }
107
128
 
@@ -130,20 +151,14 @@ export class Application extends BaseApplication<
130
151
  return;
131
152
  }
132
153
 
133
- if (this.error && data.payload.code === 'APP_RUNNING') {
134
- this.maintained = true;
135
- this.setMaintaining(false);
136
- this.error = null;
137
- globalThis.window.location.reload();
138
- return;
139
- }
140
-
141
154
  const maintaining = data.type === 'maintaining' && data.payload.code !== 'APP_RUNNING';
142
- console.log('ws:message', { maintaining, data });
143
155
  if (maintaining) {
144
156
  this.setMaintaining(true);
145
157
  this.error = data.payload;
146
158
  } else {
159
+ if (this.hasLoadError) {
160
+ globalThis.window.location.reload();
161
+ }
147
162
  this.setMaintaining(false);
148
163
  this.maintained = true;
149
164
  this.error = null;
@@ -193,4 +208,28 @@ export class Application extends BaseApplication<
193
208
  addFieldInterfaceOperator(name: string, operatorOption: any) {
194
209
  return this.dataSourceManager.addFieldInterfaceOperator(name, operatorOption);
195
210
  }
211
+
212
+ registerFieldFilterOperator(operator: any) {
213
+ return this.dataSourceManager.registerFieldFilterOperator(operator);
214
+ }
215
+
216
+ registerFieldFilterOperatorGroup(name: string, operators: any[] = []) {
217
+ return this.dataSourceManager.registerFieldFilterOperatorGroup(name, operators);
218
+ }
219
+
220
+ addFieldFilterOperatorsToGroup(name: string, operators: any[] = []) {
221
+ return this.dataSourceManager.addFieldFilterOperatorsToGroup(name, operators);
222
+ }
223
+
224
+ registerFieldValidationConfigure(item: any) {
225
+ return this.dataSourceManager.collectionFieldInterfaceManager?.registerFieldValidationConfigure?.(item);
226
+ }
227
+
228
+ registerFieldValidationConfigureGroup(name: string, items: any[] = []) {
229
+ return this.dataSourceManager.collectionFieldInterfaceManager?.registerFieldValidationConfigureGroup?.(name, items);
230
+ }
231
+
232
+ addFieldValidationConfiguresToGroup(name: string, items: any[] = []) {
233
+ return this.dataSourceManager.collectionFieldInterfaceManager?.addFieldValidationConfiguresToGroup?.(name, items);
234
+ }
196
235
  }
@@ -25,11 +25,13 @@ import { I18nextProvider } from 'react-i18next';
25
25
  import { ErrorBoundary } from 'react-error-boundary';
26
26
  import { Link, NavLink, Navigate } from 'react-router-dom';
27
27
  import { isValidElementType } from 'react-is';
28
+ import type { AppListProps } from '@ant-design/pro-layout/es/components/AppsLogoComponents/types';
28
29
  import AntdAppProvider from './theme/AntdAppProvider';
29
30
  import { GlobalThemeProvider } from './theme';
30
31
  import { AIManager } from './ai';
31
32
  import { AppError, AppMaintaining, AppMaintainingDialog, AppNotFound, AppSpin, BlankComponent } from './components';
32
33
  import { SystemSettingsSource } from './flow/system-settings';
34
+ import { LayoutManager } from './layout-manager/LayoutManager';
33
35
  import type { PluginClass, PluginManager, PluginType } from './PluginManager';
34
36
  import { RouteRepository } from './RouteRepository';
35
37
  import type {
@@ -54,6 +56,7 @@ declare global {
54
56
  }
55
57
 
56
58
  type AnyComponent = RenderableComponentType<any>;
59
+ type AppListLoader = (app: BaseApplication<any>) => Promise<AppListProps> | AppListProps;
57
60
  type AuthTokenPayload = {
58
61
  token: string;
59
62
  authenticator: string | null;
@@ -122,6 +125,7 @@ export abstract class BaseApplication<
122
125
  public components: Record<string, AnyComponent> = {};
123
126
  public pluginManager: TPluginManager;
124
127
  public pluginSettingsManager: TPluginSettingsManager;
128
+ public layoutManager: LayoutManager<this>;
125
129
  public aiManager!: AIManager;
126
130
  public devDynamicImport?: DevDynamicImport;
127
131
  public requirejs!: RequireJS;
@@ -153,8 +157,10 @@ export abstract class BaseApplication<
153
157
  private wsAuthorized = false;
154
158
  apps: {
155
159
  Component?: AnyComponent | null;
160
+ loadAppList?: AppListLoader | null;
156
161
  } = {
157
162
  Component: null,
163
+ loadAppList: null,
158
164
  };
159
165
 
160
166
  get pm(): TPluginManager {
@@ -169,6 +175,18 @@ export abstract class BaseApplication<
169
175
  return this.wsAuthorized;
170
176
  }
171
177
 
178
+ public setDocumentLanguage(language?: string | null) {
179
+ if (typeof document === 'undefined') {
180
+ return;
181
+ }
182
+
183
+ if (language) {
184
+ document.documentElement.lang = language;
185
+ } else {
186
+ document.documentElement.removeAttribute('lang');
187
+ }
188
+ }
189
+
172
190
  constructor(protected options: TOptions = {} as TOptions) {
173
191
  this.initRequireJs();
174
192
  this.defineObservableState();
@@ -180,6 +198,7 @@ export abstract class BaseApplication<
180
198
  this.initializeExtendedState();
181
199
  this.i18n = this.createI18n(options);
182
200
  this.router = this.createRouterManager(options);
201
+ this.layoutManager = this.createLayoutManager(options);
183
202
  this.pluginManager = this.createPluginManager(options);
184
203
  this.flowEngine = new FlowEngine();
185
204
  this.flowEngine.registerModels({ ApplicationModel });
@@ -205,6 +224,7 @@ export abstract class BaseApplication<
205
224
  this.addRoutes();
206
225
  this.i18n.on('languageChanged', (lng) => {
207
226
  this.apiClient.auth.locale = lng;
227
+ this.setDocumentLanguage(lng);
208
228
  });
209
229
  this.initListeners();
210
230
  this.afterManagersInitialized();
@@ -218,6 +238,7 @@ export abstract class BaseApplication<
218
238
  maintained: observable.ref,
219
239
  maintaining: observable.ref,
220
240
  error: observable.ref,
241
+ apps: observable,
221
242
  });
222
243
  }
223
244
 
@@ -517,6 +538,14 @@ export abstract class BaseApplication<
517
538
  });
518
539
  }
519
540
 
541
+ setAppsComponent({ Component }: { Component: AnyComponent }) {
542
+ this.apps.Component = Component;
543
+ }
544
+
545
+ setAppsProvider({ loadAppList }: { loadAppList: AppListLoader }) {
546
+ this.apps.loadAppList = loadAppList;
547
+ }
548
+
520
549
  protected getRootFallback() {
521
550
  return this.renderComponent('AppSpin');
522
551
  }
@@ -554,6 +583,9 @@ export abstract class BaseApplication<
554
583
  protected abstract createRouterManager(options: TOptions): TRouterManager;
555
584
  protected abstract createPluginManager(options: TOptions): TPluginManager;
556
585
  protected abstract createPluginSettingsManager(options: TOptions): TPluginSettingsManager;
586
+ protected createLayoutManager(_options: TOptions) {
587
+ return new LayoutManager(this);
588
+ }
557
589
  protected createWebSocketClient(options: TOptions) {
558
590
  return new WebSocketClient(options.ws ?? false);
559
591
  }
@@ -524,7 +524,7 @@ export class PluginSettingsManager<TApp extends BaseApplication<any> = BaseAppli
524
524
 
525
525
  if (page.key === 'index') {
526
526
  this.app.router.add(this.getRouteName(page.name), {
527
- index: true,
527
+ path: '',
528
528
  Component: fallbackComponent,
529
529
  componentLoader: page.componentLoader,
530
530
  });
@@ -50,10 +50,25 @@ export interface RouteType extends Omit<RouteObject, 'children' | 'Component'> {
50
50
  Component?: ComponentTypeAndString;
51
51
  componentLoader?: ComponentLoader;
52
52
  skipAuthCheck?: boolean;
53
+ authCheck?: boolean;
53
54
  }
54
55
  export type RenderComponentType = (Component: ComponentTypeAndString, props?: any) => React.ReactNode;
55
56
  export type RouterComponentType = React.FC<{ BaseLayout?: ComponentType }>;
56
57
 
58
+ function removeBasename(pathname: string, basename?: string) {
59
+ if (!basename || basename === '/') {
60
+ return pathname;
61
+ }
62
+ const normalizedBasename = basename.replace(/\/$/, '');
63
+ if (pathname === normalizedBasename) {
64
+ return '/';
65
+ }
66
+ if (pathname.startsWith(`${normalizedBasename}/`)) {
67
+ return pathname.slice(normalizedBasename.length) || '/';
68
+ }
69
+ return pathname;
70
+ }
71
+
57
72
  export class RouterManager<TApp extends BaseApplication<any> = BaseApplication<any>> {
58
73
  protected routes: Record<string, RouteType> = {};
59
74
  protected options: RouterOptions;
@@ -186,8 +201,9 @@ export class RouterManager<TApp extends BaseApplication<any> = BaseApplication<a
186
201
 
187
202
  matchRoutes(pathname: string) {
188
203
  const routes = this.getRoutesTree();
204
+ const basename = this.router?.basename || this.getBasename();
189
205
  // @ts-ignore
190
- return matchRoutes<RouteType>(routes, pathname, this.basename);
206
+ return matchRoutes<RouteType>(routes, removeBasename(pathname, basename));
191
207
  }
192
208
 
193
209
  isSkippedAuthCheckRoute(pathname: string) {
@@ -9,6 +9,8 @@
9
9
 
10
10
  import React from 'react';
11
11
  import { createMockClient } from '@nocobase/client-v2';
12
+ import { createMemoryRouter } from 'react-router-dom';
13
+ import type { RouteObject } from 'react-router-dom';
12
14
 
13
15
  describe('PluginSettingsManager v2', () => {
14
16
  it('should return menu -> page two-level structure', () => {
@@ -72,7 +74,7 @@ describe('PluginSettingsManager v2', () => {
72
74
  expect(app.pluginSettingsManager.getRoutePath('demo.advanced')).toBe('/admin/settings/demo/advanced');
73
75
 
74
76
  expect(app.router.get('admin.settings.demo')).toMatchObject({ path: 'demo' });
75
- expect(app.router.get('admin.settings.demo.index')).toMatchObject({ index: true });
77
+ expect(app.router.get('admin.settings.demo.index')).toMatchObject({ path: '' });
76
78
  expect(app.router.get('admin.settings.demo.advanced')).toMatchObject({ path: 'advanced' });
77
79
  });
78
80
 
@@ -110,7 +112,44 @@ describe('PluginSettingsManager v2', () => {
110
112
  });
111
113
 
112
114
  expect(app.pluginSettingsManager.get('demo.index')).toMatchObject({ componentLoader });
113
- expect(app.router.get('admin.settings.demo.index')).toMatchObject({ componentLoader, index: true });
115
+ expect(app.router.get('admin.settings.demo.index')).toMatchObject({ componentLoader, path: '' });
116
+ });
117
+
118
+ it('should allow nested routes under index page route', () => {
119
+ const app = createMockClient();
120
+ const findRoute = (routes: RouteObject[], routeId: string): RouteObject | null => {
121
+ for (const route of routes) {
122
+ if (route.id === routeId) {
123
+ return route;
124
+ }
125
+ const matched = route.children ? findRoute(route.children, routeId) : null;
126
+ if (matched) {
127
+ return matched;
128
+ }
129
+ }
130
+ return null;
131
+ };
132
+
133
+ app.pluginSettingsManager.addMenuItem({ key: 'demo', title: 'Demo' });
134
+ app.pluginSettingsManager.addPageTabItem({ menuKey: 'demo', key: 'index', title: 'Overview' });
135
+ app.router.add('admin.settings.demo.index.layout', {
136
+ path: 'configure',
137
+ Component: () => React.createElement('div', null, 'configure'),
138
+ });
139
+
140
+ const routes = app.router.getRoutesTree();
141
+ const indexRoute = findRoute(routes, 'admin.settings.demo.index');
142
+
143
+ expect(indexRoute).toMatchObject({
144
+ id: 'admin.settings.demo.index',
145
+ path: '',
146
+ });
147
+ expect(indexRoute).not.toHaveProperty('index');
148
+ expect(findRoute(routes, 'admin.settings.demo.index.layout')).toMatchObject({
149
+ id: 'admin.settings.demo.index.layout',
150
+ path: 'configure',
151
+ });
152
+ expect(() => createMemoryRouter(routes, { initialEntries: ['/demo/configure/form-1'] })).not.toThrow();
114
153
  });
115
154
 
116
155
  it('should merge duplicate registration and refresh route config', () => {
@@ -33,6 +33,7 @@ describe('app', () => {
33
33
 
34
34
  afterEach(() => {
35
35
  document.querySelectorAll('link[rel="shortcut icon"]').forEach((node) => node.remove());
36
+ document.documentElement.removeAttribute('lang');
36
37
  vi.restoreAllMocks();
37
38
  });
38
39
 
@@ -89,6 +90,14 @@ describe('app', () => {
89
90
  expect(favicon.getAttribute('href')).toBe('/favicon/favicon.ico');
90
91
  });
91
92
 
93
+ it('should sync document language when app language changes', async () => {
94
+ const app = new Application({ router });
95
+
96
+ await app.i18n.changeLanguage('ja-JP');
97
+
98
+ expect(document.documentElement.lang).toBe('ja-JP');
99
+ });
100
+
92
101
  it('should escape app version html placeholder content', () => {
93
102
  expect(getAppVersionHTML('<script>alert(1)</script>&"')).toBe(
94
103
  '<span class="nb-app-version">v&lt;script&gt;alert(1)&lt;/script&gt;&amp;&quot;</span>',
@@ -234,6 +243,10 @@ describe('app', () => {
234
243
  });
235
244
  await renderApp(app);
236
245
  expect(await screen.findByText('Hello Basename Route')).toBeInTheDocument();
246
+ expect(app.router.matchRoutes('/v2/demo/app-info')?.some((match) => match.route.path === '/demo/app-info')).toBe(
247
+ true,
248
+ );
249
+ expect(app.router.matchRoutes('/demo/app-info')?.some((match) => match.route.path === '/demo/app-info')).toBe(true);
237
250
  });
238
251
 
239
252
  it('should support plugin settings componentLoader lazy functionality', async () => {
@@ -370,7 +383,10 @@ describe('app', () => {
370
383
 
371
384
  await waitFor(() => expect(screen.queryByText('maintaining error message')).not.toBeInTheDocument());
372
385
  expect(screen.getByText('Hello')).toBeInTheDocument();
373
- expect(reloadMock).toHaveBeenCalled();
386
+ // Aligned with v1: a routine maintaining→APP_RUNNING cycle does not
387
+ // reload the page. Only `hasLoadError === true` (set when the initial
388
+ // `app.load()` itself fails) triggers a recovery reload.
389
+ expect(reloadMock).not.toHaveBeenCalled();
374
390
  } finally {
375
391
  Object.defineProperty(globalThis.window, 'location', {
376
392
  configurable: true,
@@ -28,6 +28,7 @@ describe('client-v2 defineGlobalDeps', () => {
28
28
  expect(define).toHaveBeenCalledWith('@nocobase/evaluators/client', expect.any(Function));
29
29
  expect(define).toHaveBeenCalledWith('@dnd-kit/core', expect.any(Function));
30
30
  expect(define).toHaveBeenCalledWith('@dnd-kit/sortable', expect.any(Function));
31
+ expect(define).toHaveBeenCalledWith('@ctrl/tinycolor', expect.any(Function));
31
32
  expect(define).toHaveBeenCalledWith('ahooks', expect.any(Function));
32
33
  expect(define).toHaveBeenCalledWith('dayjs', expect.any(Function));
33
34
  expect(define).toHaveBeenCalledWith('lodash', expect.any(Function));
@@ -7,11 +7,21 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import { createMockClient } from '@nocobase/client-v2';
11
- import { render, screen, waitFor } from '@testing-library/react';
10
+ import { createMockClient, Plugin } from '@nocobase/client-v2';
11
+ import { act, render, screen, waitFor } from '@testing-library/react';
12
12
  import React from 'react';
13
13
  import { NocoBaseBuildInPlugin } from '../nocobase-buildin-plugin';
14
14
 
15
+ class SkippedPublicRoutePlugin extends Plugin {
16
+ async load() {
17
+ this.router.add('public', {
18
+ path: '/public',
19
+ skipAuthCheck: true,
20
+ Component: () => <div>public page</div>,
21
+ });
22
+ }
23
+ }
24
+
15
25
  describe('nocobase buildin plugin auth redirect', () => {
16
26
  const originalLocation = globalThis.window.location;
17
27
 
@@ -107,6 +117,39 @@ describe('nocobase buildin plugin auth redirect', () => {
107
117
  });
108
118
  });
109
119
 
120
+ it('should check current user after navigating from skipped route to v2 admin', async () => {
121
+ const app = createMockClient({
122
+ publicPath: '/v2/',
123
+ plugins: [NocoBaseBuildInPlugin as any, SkippedPublicRoutePlugin as any],
124
+ router: { type: 'memory', initialEntries: ['/v2/public'] },
125
+ });
126
+ app.apiMock.onGet('app:getLang').reply(200, {
127
+ data: { lang: 'en-US', resources: { client: {} }, cron: {} },
128
+ });
129
+ app.apiMock.onGet('/auth:check').reply(200, { data: {} });
130
+
131
+ const Root = app.getRootComponent();
132
+ render(<Root />);
133
+
134
+ expect(await screen.findByText('public page')).toBeInTheDocument();
135
+ const authCheckRequestsBeforeNavigation = app.apiMock.history.get.filter(
136
+ (request) => request.url === '/auth:check',
137
+ ).length;
138
+ expect(authCheckRequestsBeforeNavigation).toBe(0);
139
+
140
+ await act(async () => {
141
+ await app.router.router.navigate('/v2/admin');
142
+ });
143
+
144
+ await waitFor(() => {
145
+ expect(app.apiMock.history.get.filter((request) => request.url === '/auth:check').length).toBeGreaterThan(
146
+ authCheckRequestsBeforeNavigation,
147
+ );
148
+ expect(app.router.router.state.location.pathname).toBe('/v2/signin');
149
+ expect(app.router.router.state.location.search).toBe('?redirect=%2Fv2%2Fadmin');
150
+ });
151
+ });
152
+
110
153
  it('should render v2 admin root without redirecting away', async () => {
111
154
  const app = createMockClient({
112
155
  publicPath: '/v2/',
@@ -0,0 +1,177 @@
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 { ACLRolesCheckProvider, createMockClient, Plugin } from '@nocobase/client-v2';
11
+ import { fireEvent, render, screen, waitFor, within } from '@testing-library/react';
12
+ import React from 'react';
13
+ import { NocoBaseBuildInPlugin } from '../nocobase-buildin-plugin';
14
+
15
+ class TestAclPlugin extends Plugin {
16
+ async load() {
17
+ this.app.use(ACLRolesCheckProvider);
18
+ }
19
+ }
20
+
21
+ type MockClientApplication = ReturnType<typeof createMockClient>;
22
+
23
+ const renderApp = (app: MockClientApplication) => {
24
+ const Root = app.getRootComponent();
25
+ render(<Root />);
26
+ };
27
+
28
+ const waitForGetRequests = async (app: MockClientApplication, urls: string[]) => {
29
+ await waitFor(
30
+ () => {
31
+ const history = app.apiMock.history.get.map((request) => request.url);
32
+ expect(history).toEqual(expect.arrayContaining(urls));
33
+ },
34
+ { timeout: 3000 },
35
+ );
36
+ };
37
+
38
+ const setupApp = (pmList: any[]) => {
39
+ const app = createMockClient({
40
+ plugins: [NocoBaseBuildInPlugin, TestAclPlugin],
41
+ router: { type: 'memory', initialEntries: ['/admin/settings/plugin-manager'] },
42
+ });
43
+
44
+ app.apiMock.onGet('/auth:check').reply(200, {
45
+ data: { id: 1, nickname: 'Super Admin' },
46
+ });
47
+ app.apiMock.onGet('app:getLang').reply(200, {
48
+ data: { lang: 'en-US', resources: { client: {} }, cron: {} },
49
+ });
50
+ app.apiMock.onGet('app:getInfo').reply(200, { data: { id: 'mock-app' } });
51
+ app.apiMock.onGet('roles:check').reply(200, {
52
+ data: { role: 'root', snippets: ['pm', 'pm.system-settings.system-settings'] },
53
+ });
54
+ app.apiMock.onGet('/desktopRoutes:listAccessible').reply(200, { data: [] });
55
+ app.apiMock.onGet('systemSettings:get').reply(200, {
56
+ data: {
57
+ id: 1,
58
+ title: 'NocoBase',
59
+ raw_title: 'NocoBase',
60
+ enabledLanguages: ['en-US'],
61
+ logo: null,
62
+ },
63
+ });
64
+ app.apiMock.onGet('pm:list').reply(200, { data: pmList });
65
+ app.apiMock.onGet('pm:listEnabledV2').reply(200, { data: [] });
66
+
67
+ // pm:* mutations default to GET in axios when called via api.request without method
68
+ app.apiMock.onGet('pm:enable').reply(200, { data: {} });
69
+ app.apiMock.onGet('pm:disable').reply(200, { data: {} });
70
+ app.apiMock.onGet('pm:remove').reply(200, { data: {} });
71
+
72
+ return app;
73
+ };
74
+
75
+ describe('plugin-manager page', () => {
76
+ it('fires pm:enable when toggling switch on a disabled plugin', async () => {
77
+ const app = setupApp([
78
+ {
79
+ name: 'demo-plugin',
80
+ packageName: '@nocobase/demo-plugin',
81
+ displayName: 'Demo plugin',
82
+ description: 'A demo',
83
+ enabled: false,
84
+ builtIn: false,
85
+ removable: true,
86
+ version: '0.1.0',
87
+ isCompatible: true,
88
+ keywords: [],
89
+ },
90
+ ]);
91
+
92
+ renderApp(app);
93
+ await waitForGetRequests(app, ['/auth:check', 'roles:check', 'pm:list']);
94
+
95
+ const card = await screen.findByRole('button', { name: 'Demo plugin' });
96
+ const switchControl = within(card.closest('.ant-card') as HTMLElement).getByRole('switch');
97
+ expect(switchControl).toHaveAttribute('aria-checked', 'false');
98
+
99
+ fireEvent.click(switchControl);
100
+
101
+ await waitFor(() => {
102
+ const enableCall = app.apiMock.history.get.find((req) => req.url === 'pm:enable');
103
+ expect(enableCall).toBeDefined();
104
+ expect(enableCall?.params).toMatchObject({ filterByTk: 'demo-plugin' });
105
+ });
106
+ });
107
+
108
+ it('fires pm:disable after confirm when toggling switch on an enabled plugin', async () => {
109
+ const app = setupApp([
110
+ {
111
+ name: 'demo-plugin',
112
+ packageName: '@nocobase/demo-plugin',
113
+ displayName: 'Demo plugin',
114
+ description: 'A demo',
115
+ enabled: true,
116
+ builtIn: false,
117
+ removable: true,
118
+ version: '0.1.0',
119
+ isCompatible: true,
120
+ keywords: [],
121
+ },
122
+ ]);
123
+
124
+ renderApp(app);
125
+ await waitForGetRequests(app, ['/auth:check', 'roles:check', 'pm:list']);
126
+
127
+ const card = await screen.findByRole('button', { name: 'Demo plugin' });
128
+ const switchControl = within(card.closest('.ant-card') as HTMLElement).getByRole('switch');
129
+ expect(switchControl).toHaveAttribute('aria-checked', 'true');
130
+
131
+ fireEvent.click(switchControl);
132
+
133
+ const confirmTitle = await screen.findByText('Are you sure to disable this plugin?');
134
+ const confirmDialog = confirmTitle.closest('.ant-modal-confirm') as HTMLElement;
135
+ expect(confirmDialog).not.toBeNull();
136
+ const okButton = within(confirmDialog).getByText('OK');
137
+ fireEvent.click(okButton);
138
+
139
+ await waitFor(() => {
140
+ const disableCall = app.apiMock.history.get.find((req) => req.url === 'pm:disable');
141
+ expect(disableCall).toBeDefined();
142
+ expect(disableCall?.params).toMatchObject({ filterByTk: 'demo-plugin' });
143
+ });
144
+ });
145
+
146
+ it('fires pm:remove after Popconfirm on a removable plugin', async () => {
147
+ const app = setupApp([
148
+ {
149
+ name: 'demo-plugin',
150
+ packageName: '@nocobase/demo-plugin',
151
+ displayName: 'Demo plugin',
152
+ description: 'A demo',
153
+ enabled: false,
154
+ builtIn: false,
155
+ removable: true,
156
+ version: '0.1.0',
157
+ isCompatible: true,
158
+ keywords: [],
159
+ },
160
+ ]);
161
+
162
+ renderApp(app);
163
+ await waitForGetRequests(app, ['/auth:check', 'roles:check', 'pm:list']);
164
+
165
+ const removeLink = await screen.findByText('Remove');
166
+ fireEvent.click(removeLink);
167
+
168
+ const yesButton = await screen.findByRole('button', { name: 'Yes' });
169
+ fireEvent.click(yesButton);
170
+
171
+ await waitFor(() => {
172
+ const removeCall = app.apiMock.history.get.find((req) => req.url === 'pm:remove');
173
+ expect(removeCall).toBeDefined();
174
+ expect(removeCall?.params).toMatchObject({ filterByTk: 'demo-plugin' });
175
+ });
176
+ });
177
+ });