@gadmin2n/schematics 0.0.64 → 0.0.66

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 (198) hide show
  1. package/dist/lib/application/files/gadmin2-game-angle-demo/.dockerignore +2 -2
  2. package/dist/lib/application/files/gadmin2-game-angle-demo/Dockerfile +40 -26
  3. package/dist/lib/application/files/gadmin2-game-angle-demo/Jenkinsfile +30 -4
  4. package/dist/lib/application/files/gadmin2-game-angle-demo/config/prisma/example.prisma +33 -0
  5. package/dist/lib/application/files/gadmin2-game-angle-demo/config/prisma/system.prisma +170 -0
  6. package/dist/lib/application/files/gadmin2-game-angle-demo/config/ui/Event.ts +70 -0
  7. package/dist/lib/application/files/gadmin2-game-angle-demo/config/ui/Game.ts +6 -6
  8. package/dist/lib/application/files/gadmin2-game-angle-demo/config/ui/ITActivityDay.ts +70 -0
  9. package/dist/lib/application/files/gadmin2-game-angle-demo/config/ui/Log.ts +2 -2
  10. package/dist/lib/application/files/gadmin2-game-angle-demo/config/ui/Role.ts +2 -2
  11. package/dist/lib/application/files/gadmin2-game-angle-demo/server/.env +20 -9
  12. package/dist/lib/application/files/gadmin2-game-angle-demo/server/.env.local +1 -0
  13. package/dist/lib/application/files/gadmin2-game-angle-demo/server/.eslintrc.js +1 -0
  14. package/dist/lib/application/files/gadmin2-game-angle-demo/server/.prettierignore +1 -0
  15. package/dist/lib/application/files/gadmin2-game-angle-demo/server/README.md +2 -18
  16. package/dist/lib/application/files/gadmin2-game-angle-demo/server/gadmin-cli.json +1 -1
  17. package/dist/lib/application/files/gadmin2-game-angle-demo/server/migrate-between-pg-schemas.js +1232 -0
  18. package/dist/lib/application/files/gadmin2-game-angle-demo/server/package.json +65 -38
  19. package/dist/lib/application/files/gadmin2-game-angle-demo/server/prisma/.generator.prisma +4 -3
  20. package/dist/lib/application/files/gadmin2-game-angle-demo/server/prisma.config.ts +16 -0
  21. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/games.ts +1 -71
  22. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/index.ts +17 -21
  23. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/permissions.ts +278 -0
  24. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/seedDataMngtPages.ts +258 -0
  25. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/users.ts +7 -0
  26. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/alias.config.ts +7 -0
  27. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/app.controller.ts +151 -11
  28. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/app.module.ts +29 -13
  29. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/app.service.ts +151 -12
  30. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/lib/auth.guard.ts +87 -41
  31. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/lib/http-cache.interceptor.ts +21 -0
  32. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/lib/logger.ts +19 -0
  33. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/lib/safe-log.util.ts +176 -0
  34. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/lib/{yufuid.ts → taihu.ts} +49 -34
  35. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/lib/tracing.ts +174 -0
  36. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/lib/trim.pipe.ts +51 -0
  37. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/lib/utils.ts +91 -0
  38. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/lib/woaAuth.ts +25 -12
  39. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/main.ts +22 -12
  40. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/audit/audit.controller.spec.ts +20 -0
  41. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/audit/audit.controller.ts +190 -0
  42. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/audit/audit.module.ts +10 -0
  43. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/audit/audit.service.spec.ts +338 -0
  44. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/audit/audit.service.ts +83 -0
  45. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/game/game.controller.spec.ts +20 -0
  46. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/game/game.controller.ts +188 -0
  47. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/game/game.module.ts +10 -0
  48. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/game/game.service.spec.ts +18 -0
  49. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/game/game.service.ts +83 -0
  50. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/page/page.controller.spec.ts +20 -0
  51. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/page/page.controller.ts +250 -0
  52. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/page/page.module.ts +10 -0
  53. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/page/page.service.spec.ts +18 -0
  54. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/page/page.service.ts +1051 -0
  55. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/pageResource/pageResource.controller.spec.ts +20 -0
  56. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/pageResource/pageResource.controller.ts +196 -0
  57. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/pageResource/pageResource.module.ts +13 -0
  58. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/pageResource/pageResource.service.spec.ts +18 -0
  59. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/pageResource/pageResource.service.ts +219 -0
  60. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/resource/resource.controller.spec.ts +20 -0
  61. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/resource/resource.controller.ts +196 -0
  62. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/resource/resource.module.ts +10 -0
  63. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/resource/resource.service.spec.ts +18 -0
  64. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/resource/resource.service.ts +199 -0
  65. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/role/role.controller.spec.ts +20 -0
  66. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/role/role.controller.ts +210 -0
  67. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/role/role.module.ts +12 -0
  68. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/role/role.service.spec.ts +18 -0
  69. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/role/role.service.ts +849 -0
  70. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/role/roles-refresher.service.ts +133 -0
  71. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/rolePages/rolePages.controller.spec.ts +20 -0
  72. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/rolePages/rolePages.controller.ts +196 -0
  73. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/rolePages/rolePages.module.ts +10 -0
  74. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/rolePages/rolePages.service.spec.ts +18 -0
  75. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/rolePages/rolePages.service.ts +201 -0
  76. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/roleResource/roleResource.controller.spec.ts +20 -0
  77. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/roleResource/roleResource.controller.ts +196 -0
  78. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/roleResource/roleResource.module.ts +10 -0
  79. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/roleResource/roleResource.service.spec.ts +18 -0
  80. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/roleResource/roleResource.service.ts +216 -0
  81. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.controller.spec.ts +20 -0
  82. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.controller.ts +198 -0
  83. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.module.ts +10 -0
  84. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.service.spec.ts +18 -0
  85. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.service.ts +104 -0
  86. package/dist/lib/application/files/gadmin2-game-angle-demo/server/start-prod.sh +130 -0
  87. package/dist/lib/application/files/gadmin2-game-angle-demo/server/tsconfig.json +18 -3
  88. package/dist/lib/application/files/gadmin2-game-angle-demo/web/index.html +19 -0
  89. package/dist/lib/application/files/gadmin2-game-angle-demo/web/package.json +34 -42
  90. package/dist/lib/application/files/gadmin2-game-angle-demo/web/postcss.config.cjs +6 -0
  91. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/App.tsx +111 -185
  92. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/auditLogProvider.ts +5 -5
  93. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/authProvider.ts +2 -2
  94. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/SqlModal.tsx +419 -0
  95. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/contexts/business/index.tsx +1 -1
  96. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/contexts/color-mode/index.tsx +49 -51
  97. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/custom-avatar.tsx +38 -0
  98. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/index.ts +4 -0
  99. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/layout/{header/index.tsx → header.tsx} +22 -31
  100. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/layout/index.ts +3 -1
  101. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/layout/layout.tsx +32 -0
  102. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/layout/logo.tsx +19 -0
  103. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/layout/sider.tsx +331 -166
  104. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/layout/title.tsx +61 -0
  105. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/pagination-total.tsx +21 -0
  106. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/tags/index.ts +1 -0
  107. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/tags/role-tag.tsx +44 -0
  108. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/text.tsx +74 -0
  109. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/config/http.ts +28 -0
  110. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/config/routeRegistry.tsx +258 -0
  111. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/constants/layout.ts +16 -0
  112. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/enums/audit-log.enum.ts +13 -0
  113. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/enums/index.ts +1 -0
  114. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/helpers/get-name-initials.ts +8 -0
  115. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/helpers/get-random-color.ts +27 -0
  116. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/helpers/http.ts +87 -0
  117. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/helpers/index.tsx +6 -1
  118. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/helpers/login.ts +22 -59
  119. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/helpers/utils.tsx +5 -0
  120. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/hooks/useDynamicResources.tsx +211 -0
  121. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/hooks/useFetchData.ts +33 -0
  122. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/hooks/useRoles.ts +30 -0
  123. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/hooks/useUserPageAccess.ts +339 -0
  124. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/i18n.ts +8 -4
  125. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/index.tsx +3 -11
  126. package/dist/lib/application/files/gadmin2-game-angle-demo/web/{public → src}/locales/en/common.json +1 -1
  127. package/dist/lib/application/files/gadmin2-game-angle-demo/web/{public → src}/locales/zh_CN/common.json +1 -1
  128. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/audit/components/action-cell.css +3 -0
  129. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/audit/components/action-cell.tsx +134 -0
  130. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/audit/create.tsx +113 -0
  131. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/audit/edit.tsx +122 -0
  132. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/audit/index.ts +8 -0
  133. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/audit/index.tsx +6 -0
  134. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/audit/list.tsx +213 -0
  135. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/audit/show.tsx +61 -0
  136. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/page/Components/AssignRolesModal.tsx +168 -0
  137. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/page/Components/CreatePageModal.tsx +42 -0
  138. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/page/Components/EditPageModal.tsx +42 -0
  139. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/page/Components/PageDetailDrawer.tsx +101 -0
  140. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/page/Components/PageFormModal.tsx +731 -0
  141. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/page/hooks/usePageManagement.ts +36 -0
  142. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/page/index.ts +1 -0
  143. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/page/list.tsx +1215 -0
  144. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/page/queries.ts +17 -0
  145. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/page/types.ts +45 -0
  146. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/permissionReadme/index.tsx +1089 -0
  147. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/resource/Components/CreateModal.tsx +25 -0
  148. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/resource/Components/EditModal.tsx +28 -0
  149. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/resource/Components/ResourceDetailDrawer.tsx +160 -0
  150. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/resource/Components/modal.tsx +202 -0
  151. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/resource/index.ts +2 -0
  152. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/resource/list.tsx +212 -0
  153. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/resource/queries.ts +10 -0
  154. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/resource/types.ts +9 -0
  155. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/role/Components/CreateModal.tsx +30 -0
  156. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/role/Components/EditModal.tsx +47 -0
  157. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/role/Components/RoleDetailDrawer.tsx +56 -0
  158. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/role/Components/modal.tsx +302 -0
  159. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/role/hooks/useRolePage.ts +35 -0
  160. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/role/index.ts +1 -0
  161. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/role/list.tsx +431 -0
  162. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/role/queries.ts +8 -0
  163. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/role/types.ts +8 -0
  164. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/user/components/create-modal.tsx +17 -0
  165. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/user/components/edit-modal.tsx +19 -0
  166. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/user/components/form-modal.tsx +188 -0
  167. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/user/components/index.ts +5 -0
  168. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/user/components/role-tag.tsx +48 -0
  169. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/user/components/show-drawer.tsx +140 -0
  170. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/user/index.ts +1 -0
  171. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/user/list.tsx +372 -0
  172. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/user/queries.ts +14 -0
  173. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/styles/antd.css +132 -0
  174. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/styles/fc.css +58 -0
  175. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/styles/index.css +128 -0
  176. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/styles/show-drawer.module.css +18 -0
  177. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/styles/show-page.module.css +21 -0
  178. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/types/audit-log.ts +1 -0
  179. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/types/index.ts +3 -0
  180. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/types/role.ts +7 -0
  181. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/types/user.ts +1 -0
  182. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/vite-env.d.ts +1 -0
  183. package/dist/lib/application/files/gadmin2-game-angle-demo/web/tsconfig.json +5 -4
  184. package/dist/lib/application/files/gadmin2-game-angle-demo/web/vite.config.ts +31 -0
  185. package/dist/lib/application/files/gadmin2-game-angle-demo/web/yarn.lock +8321 -0
  186. package/package.json +1 -1
  187. package/dist/lib/application/files/gadmin2-game-angle-demo/config/prisma/sample.prisma +0 -65
  188. package/dist/lib/application/files/gadmin2-game-angle-demo/config/ui/Source.ts +0 -76
  189. package/dist/lib/application/files/gadmin2-game-angle-demo/config/ui/Tasklog.ts +0 -76
  190. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/roles.ts +0 -4
  191. package/dist/lib/application/files/gadmin2-game-angle-demo/web/craco.config.js +0 -27
  192. package/dist/lib/application/files/gadmin2-game-angle-demo/web/public/index.html +0 -53
  193. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/layout/styles.ts +0 -10
  194. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/helpers/utils.ts +0 -76
  195. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/react-app-env.d.ts +0 -1
  196. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/reportWebVitals.ts +0 -15
  197. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/styles/antd.less +0 -79
  198. /package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/VanillaJSONEditor/{index.js → index.jsx} +0 -0
@@ -0,0 +1,1215 @@
1
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ import { DeleteButton, EditButton, useTable } from "@refinedev/antd";
3
+ import { useGetIdentity } from "@refinedev/core";
4
+ import { Table, Button, Space, Tooltip, Tag, Alert, Modal, message, Tabs } from "antd";
5
+ import {
6
+ SaveOutlined,
7
+ UndoOutlined,
8
+ HolderOutlined,
9
+ RightOutlined,
10
+ DownOutlined,
11
+ PlusOutlined,
12
+ EyeOutlined,
13
+ TeamOutlined,
14
+ CodeOutlined,
15
+ } from "@ant-design/icons";
16
+ import {
17
+ DndContext,
18
+ closestCenter,
19
+ KeyboardSensor,
20
+ PointerSensor,
21
+ useSensor,
22
+ useSensors,
23
+ DragEndEvent,
24
+ DragOverEvent,
25
+ } from "@dnd-kit/core";
26
+ import {
27
+ SortableContext,
28
+ arrayMove,
29
+ sortableKeyboardCoordinates,
30
+ useSortable,
31
+ verticalListSortingStrategy,
32
+ } from "@dnd-kit/sortable";
33
+ import { CSS } from "@dnd-kit/utilities";
34
+ import { restrictToVerticalAxis } from "@dnd-kit/modifiers";
35
+
36
+ import { customRequest } from "@/helpers/http";
37
+
38
+ import { AssignRolesModal } from "./Components/AssignRolesModal";
39
+ import { EditPageModal } from "./Components/EditPageModal";
40
+ import { CreatePageModal } from "./Components/CreatePageModal";
41
+ import { PageDetailDrawer } from "./Components/PageDetailDrawer";
42
+ import type {
43
+ PageMenuTreeNode,
44
+ PageResourceAssignment,
45
+ PageResourcePermissionSummary,
46
+ ResourceDefinition,
47
+ } from "./types";
48
+ import { usePageManagement } from "./hooks/usePageManagement";
49
+ import { useTranslation } from "react-i18next";
50
+ import { SqlModal, generatePageSql } from "../../components/SqlModal";
51
+
52
+ const RESOURCE_NAME_DISPLAY_TRUNCATION_LIMIT = 40;
53
+
54
+ // Sparse number gap for sortOrder (allows inserting items between existing ones)
55
+ const SORT_ORDER_GAP = 10000;
56
+
57
+ // API function for updating page order (sortOrder and parentId)
58
+ const updatePageOrder = async (
59
+ pageId: number,
60
+ updateData: { sortOrder?: number; parentId?: number | null },
61
+ ): Promise<any> => {
62
+ return customRequest(`page/updateUnique/${pageId}`, "PATCH", updateData);
63
+ };
64
+
65
+ const safelyConvertToNumber = (value: unknown): number | null => {
66
+ if (typeof value === "number" && !Number.isNaN(value)) {
67
+ return value;
68
+ }
69
+ if (typeof value === "string" && value.trim().length > 0) {
70
+ const parsedNumericValue = Number(value);
71
+ return Number.isNaN(parsedNumericValue) ? null : parsedNumericValue;
72
+ }
73
+ return null;
74
+ };
75
+
76
+ const extractPermissionActionsFromAssociation = (
77
+ associationRecord: any,
78
+ resourceRecord: any,
79
+ ): string[] => {
80
+ const aggregatedPermissionActions = new Set<string>();
81
+
82
+ const associationActions =
83
+ associationRecord?.actions ?? associationRecord?.actionList ?? associationRecord?.permissions;
84
+
85
+ if (Array.isArray(associationActions)) {
86
+ associationActions.forEach((action: any) => {
87
+ if (typeof action === "string" && action) {
88
+ aggregatedPermissionActions.add(action);
89
+ } else if (typeof action === "object" && action?.name) {
90
+ aggregatedPermissionActions.add(action.name);
91
+ } else if (typeof action === "object" && action?.code) {
92
+ aggregatedPermissionActions.add(action.code);
93
+ }
94
+ });
95
+ } else if (typeof associationActions === "string") {
96
+ associationActions
97
+ .split(",")
98
+ .map((action: string) => action.trim())
99
+ .filter(Boolean)
100
+ .forEach(action => aggregatedPermissionActions.add(action));
101
+ }
102
+
103
+ return Array.from(aggregatedPermissionActions);
104
+ };
105
+
106
+ // Draggable row component for dnd-kit
107
+ interface DraggableRowProps extends React.HTMLAttributes<HTMLTableRowElement> {
108
+ "data-row-key": string;
109
+ }
110
+
111
+ const DraggableRow: React.FC<DraggableRowProps> = ({ children, ...props }) => {
112
+ const id = props["data-row-key"];
113
+ const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
114
+ id,
115
+ });
116
+
117
+ const style: React.CSSProperties = {
118
+ ...props.style,
119
+ transform: CSS.Transform.toString(transform && { ...transform, scaleY: 1 }),
120
+ transition: transition ?? "transform 200ms ease",
121
+ ...(isDragging
122
+ ? {
123
+ position: "relative",
124
+ zIndex: 9999,
125
+ background: "#bae0ff",
126
+ boxShadow: "0 8px 24px rgba(22, 119, 255, 0.25)",
127
+ borderLeft: "3px solid #1677ff",
128
+ opacity: 0.95,
129
+ outline: "1px solid #1677ff",
130
+ }
131
+ : {}),
132
+ };
133
+
134
+ return (
135
+ <tr {...props} ref={setNodeRef} style={style} {...attributes}>
136
+ {React.Children.map(children, child => {
137
+ if ((child as React.ReactElement).key === "drag-handle") {
138
+ return React.cloneElement(child as React.ReactElement, {
139
+ children: (
140
+ <HolderOutlined
141
+ {...listeners}
142
+ style={{ cursor: "grab", color: "#999", touchAction: "none" }}
143
+ />
144
+ ),
145
+ });
146
+ }
147
+ return child;
148
+ })}
149
+ </tr>
150
+ );
151
+ };
152
+
153
+ export const PageManagementListPage: React.FC = () => {
154
+ const { i18n } = useTranslation();
155
+ const { data: identity } = useGetIdentity<any>();
156
+ const { tableProps } = useTable({
157
+ resource: "page",
158
+ pagination: {
159
+ pageSize: 20,
160
+ },
161
+ });
162
+
163
+ const {
164
+ pageListData,
165
+ refetchPageList,
166
+ pageListRequestQuery,
167
+ availableResourcesData,
168
+ refetchAvailableResources,
169
+ resourceListRequestQuery,
170
+ pageResourceAssociationsData,
171
+ refetchPageResourceAssociations,
172
+ pageResourceAssociationRequestQuery,
173
+ } = usePageManagement();
174
+
175
+ const resourceLookupByIdMap = useMemo<Record<number, ResourceDefinition>>(() => {
176
+ const resourcesArray: any[] = Array.isArray(availableResourcesData?.data)
177
+ ? availableResourcesData.data
178
+ : [];
179
+ const lookupMap: Record<number, ResourceDefinition> = {};
180
+
181
+ resourcesArray.forEach((resource: any) => {
182
+ const resourceId = safelyConvertToNumber(resource?.id);
183
+ if (resourceId !== null) {
184
+ lookupMap[resourceId] = {
185
+ id: resourceId,
186
+ code: resource?.code ?? String(resourceId),
187
+ name: resource?.name || resource?.code || `Resource ${resourceId}`,
188
+ description: resource?.description,
189
+ type: resource?.type,
190
+ };
191
+ }
192
+ });
193
+
194
+ return lookupMap;
195
+ }, [availableResourcesData]);
196
+
197
+ const availableResourcesListForModal = useMemo<ResourceDefinition[]>(() => {
198
+ return Object.values(resourceLookupByIdMap);
199
+ }, [resourceLookupByIdMap]);
200
+
201
+ const pageMenuTreeOptionsData = useMemo<PageMenuTreeNode[]>(() => {
202
+ const pagesArray: any[] = Array.isArray(pageListData?.data) ? pageListData.data : [];
203
+ const nodeMapByPageId = new Map<
204
+ number,
205
+ { treeNode: PageMenuTreeNode; parentPageId: number | null }
206
+ >();
207
+
208
+ pagesArray.forEach((page: any) => {
209
+ const pageId = safelyConvertToNumber(page?.id ?? page?.pageId ?? page?.page_id);
210
+ if (pageId === null) {
211
+ return;
212
+ }
213
+
214
+ const parentPageId =
215
+ safelyConvertToNumber(page?.parentId ?? page?.parent_id ?? page?.parent?.id) ?? null;
216
+ const pageDisplayTitle = page?.name || page?.code || `Page ${pageId}`;
217
+
218
+ nodeMapByPageId.set(pageId, {
219
+ treeNode: {
220
+ title: pageDisplayTitle,
221
+ value: String(pageId),
222
+ key: String(pageId),
223
+ children: [],
224
+ },
225
+ parentPageId,
226
+ });
227
+ });
228
+
229
+ const rootLevelNodes: PageMenuTreeNode[] = [];
230
+
231
+ nodeMapByPageId.forEach(({ treeNode, parentPageId }) => {
232
+ if (parentPageId !== null && nodeMapByPageId.has(parentPageId)) {
233
+ nodeMapByPageId.get(parentPageId)?.treeNode.children?.push(treeNode);
234
+ } else {
235
+ rootLevelNodes.push(treeNode);
236
+ }
237
+ });
238
+
239
+ const sortTreeNodesAlphabetically = (nodes: PageMenuTreeNode[]) => {
240
+ nodes.sort((nodeA, nodeB) => {
241
+ const titleA = typeof nodeA.title === "string" ? nodeA.title : String(nodeA.key);
242
+ const titleB = typeof nodeB.title === "string" ? nodeB.title : String(nodeB.key);
243
+ return titleA.localeCompare(titleB);
244
+ });
245
+ nodes.forEach(node => {
246
+ if (node.children && node.children.length) {
247
+ sortTreeNodesAlphabetically(node.children);
248
+ }
249
+ });
250
+ };
251
+
252
+ const removeEmptyChildrenArrays = (nodes: PageMenuTreeNode[]): PageMenuTreeNode[] =>
253
+ nodes.map(node => {
254
+ if (node.children && node.children.length) {
255
+ return {
256
+ ...node,
257
+ children: removeEmptyChildrenArrays(node.children),
258
+ };
259
+ }
260
+ const { children, ...nodeWithoutChildren } = node;
261
+ return nodeWithoutChildren as PageMenuTreeNode;
262
+ });
263
+
264
+ sortTreeNodesAlphabetically(rootLevelNodes);
265
+ return removeEmptyChildrenArrays(rootLevelNodes);
266
+ }, [pageListData]);
267
+
268
+ const pageResourcePermissionSummaryMap = useMemo<
269
+ Record<number, PageResourcePermissionSummary>
270
+ >(() => {
271
+ const associationsArray: any[] = Array.isArray(pageResourceAssociationsData?.data)
272
+ ? pageResourceAssociationsData.data
273
+ : [];
274
+ const summaryMapByPageId: Record<number, PageResourcePermissionSummary> = {};
275
+
276
+ associationsArray.forEach((association: any) => {
277
+ const pageId = safelyConvertToNumber(association?.pageId);
278
+ const resourceId = safelyConvertToNumber(association?.resourceId);
279
+
280
+ if (pageId === null || resourceId === null) {
281
+ return;
282
+ }
283
+
284
+ if (!summaryMapByPageId[pageId]) {
285
+ summaryMapByPageId[pageId] = {
286
+ resourceIds: [],
287
+ resourceNames: [],
288
+ allPermissionActions: [],
289
+ };
290
+ }
291
+
292
+ const pageSummary = summaryMapByPageId[pageId];
293
+
294
+ if (!pageSummary.resourceIds.includes(resourceId)) {
295
+ pageSummary.resourceIds.push(resourceId);
296
+ }
297
+
298
+ const resourceRecord = association?.resource ?? resourceLookupByIdMap[resourceId];
299
+ const resourceDisplayName =
300
+ resourceRecord?.name || resourceRecord?.code || `Resource ${resourceId}`;
301
+
302
+ if (resourceDisplayName && !pageSummary.resourceNames.includes(resourceDisplayName)) {
303
+ pageSummary.resourceNames.push(resourceDisplayName);
304
+ }
305
+
306
+ extractPermissionActionsFromAssociation(association, resourceRecord).forEach(
307
+ permissionAction => {
308
+ if (permissionAction && !pageSummary.allPermissionActions.includes(permissionAction)) {
309
+ pageSummary.allPermissionActions.push(permissionAction);
310
+ }
311
+ },
312
+ );
313
+ });
314
+
315
+ return summaryMapByPageId;
316
+ }, [pageResourceAssociationsData, resourceLookupByIdMap]);
317
+
318
+ const getPageResourceAssignmentsForModal = useCallback(
319
+ (pageId: number): PageResourceAssignment[] => {
320
+ console.log(
321
+ "Getting resource assignments for page ID:",
322
+ pageId,
323
+ pageResourceAssociationsData,
324
+ );
325
+ const associationsArray: any[] = Array.isArray(pageResourceAssociationsData?.data)
326
+ ? pageResourceAssociationsData.data
327
+ : [];
328
+
329
+ const pageAssignments: PageResourceAssignment[] = [];
330
+
331
+ associationsArray.forEach((association: any) => {
332
+ const associationPageId = safelyConvertToNumber(association?.pageId);
333
+ const resourceId = safelyConvertToNumber(association?.resourceId);
334
+
335
+ if (associationPageId !== pageId || resourceId === null) {
336
+ return;
337
+ }
338
+
339
+ const resourceRecord = association?.resource ?? resourceLookupByIdMap[resourceId];
340
+ const resourceDisplayName =
341
+ resourceRecord?.name || resourceRecord?.code || `Resource ${resourceId}`;
342
+
343
+ pageAssignments.push({
344
+ resourceId,
345
+ resourceName: resourceDisplayName,
346
+ assignedPermissions: extractPermissionActionsFromAssociation(association, resourceRecord),
347
+ });
348
+ });
349
+
350
+ return pageAssignments;
351
+ },
352
+ [pageResourceAssociationsData, resourceLookupByIdMap],
353
+ );
354
+
355
+ const [activeModalState, setActiveModalState] = useState<{
356
+ modalType: "view" | "edit" | "create" | null;
357
+ targetPageId: number | null;
358
+ defaultParentId?: number | null;
359
+ defaultPathPrefix?: string;
360
+ }>({
361
+ modalType: null,
362
+ targetPageId: null,
363
+ defaultParentId: null,
364
+ defaultPathPrefix: "",
365
+ });
366
+
367
+ // State for draggable table
368
+ const [pendingChanges, setPendingChanges] = useState<
369
+ Map<number, { sortOrder?: number; parentId?: number | null }>
370
+ >(new Map());
371
+ const [isSavingOrder, setIsSavingOrder] = useState(false);
372
+
373
+ // State for tree expand/collapse
374
+ const [expandedKeys, setExpandedKeys] = useState<Set<number>>(new Set());
375
+
376
+ // State for detail drawer
377
+ const [drawerPage, setDrawerPage] = useState<any>(null);
378
+
379
+ // State for assign roles modal
380
+ const [assignRolesTarget, setAssignRolesTarget] = useState<{
381
+ pageId: number;
382
+ pageName: string;
383
+ } | null>(null);
384
+
385
+ // State for active tab
386
+ const [activeTab, setActiveTab] = useState<string>("regular");
387
+
388
+ // State for SQL modal
389
+ const [sqlModal, setSqlModal] = useState<{
390
+ open: boolean;
391
+ title: string;
392
+ sql: string;
393
+ }>({ open: false, title: "", sql: "" });
394
+
395
+ const handleShowSql = useCallback((record: any, hasChildren: boolean) => {
396
+ const pagesArray: any[] = Array.isArray(pageListData?.data) ? pageListData.data : [];
397
+
398
+ // Build pageResources for the given page
399
+ const buildPageResources = (pageId: number) => {
400
+ const associationsArray: any[] = Array.isArray(pageResourceAssociationsData?.data)
401
+ ? pageResourceAssociationsData.data
402
+ : [];
403
+ return associationsArray
404
+ .filter((assoc: any) => safelyConvertToNumber(assoc?.pageId) === pageId)
405
+ .map((assoc: any) => {
406
+ const resourceId = safelyConvertToNumber(assoc?.resourceId);
407
+ // Prefer nested resource object, then fall back to the id-keyed lookup map (which includes code).
408
+ // The final fallback `String(resourceId)` should never be reached if availableResourcesData is loaded.
409
+ const resourceRecord = assoc?.resource ?? (resourceId !== null ? resourceLookupByIdMap[resourceId] : null);
410
+ const resourceCode = resourceRecord?.code ?? (resourceId !== null ? String(resourceId) : "unknown");
411
+ const resourceName = resourceRecord?.name || resourceRecord?.code || `Resource ${resourceId}`;
412
+ const actions = extractPermissionActionsFromAssociation(assoc, resourceRecord);
413
+ return { resourceCode, resourceName, actions };
414
+ });
415
+ };
416
+
417
+ const pageResources = buildPageResources(record.id);
418
+
419
+ // Collect child pages if this is a parent
420
+ const children: Array<{ page: any; pageResources: Array<{ resourceCode: string; resourceName: string; actions: string[] }> }> = [];
421
+ if (hasChildren) {
422
+ const childPages = pagesArray.filter(
423
+ (p: any) => safelyConvertToNumber(p?.parentId ?? p?.parent_id) === record.id,
424
+ );
425
+ for (const child of childPages) {
426
+ const childId = safelyConvertToNumber(child?.id);
427
+ if (childId === null) continue;
428
+ children.push({
429
+ page: child,
430
+ pageResources: buildPageResources(childId),
431
+ });
432
+ }
433
+ }
434
+
435
+ // Resolve parent's code from the in-memory page list so the SQL uses a
436
+ // portable subquery (SELECT id … WHERE code = '…') instead of a raw
437
+ // numeric parentId that differs between environments.
438
+ const parentId = safelyConvertToNumber(record?.parentId ?? record?.parent_id);
439
+ const parentPage = parentId !== null ? pagesArray.find((p: any) => safelyConvertToNumber(p?.id) === parentId) : null;
440
+ const recordWithParentCode = { ...record, parentCode: parentPage?.code ?? null };
441
+
442
+ const sql = generatePageSql(recordWithParentCode, pageResources, children, { authorId: identity?.userid, authorName: identity?.username });
443
+ setSqlModal({ open: true, title: `SQL — Page: ${record.name}`, sql });
444
+ }, [pageListData, pageResourceAssociationsData, resourceLookupByIdMap]);
445
+
446
+ // Ref for expand button click/double-click disambiguation
447
+ const expandClickTimer = useRef<number | null>(null);
448
+
449
+ const refreshAllPageRelatedData = useCallback(() => {
450
+ refetchPageList(pageListRequestQuery ?? {});
451
+ refetchAvailableResources(resourceListRequestQuery ?? {});
452
+ refetchPageResourceAssociations(pageResourceAssociationRequestQuery ?? {});
453
+ }, [
454
+ refetchPageList,
455
+ pageListRequestQuery,
456
+ refetchAvailableResources,
457
+ resourceListRequestQuery,
458
+ refetchPageResourceAssociations,
459
+ pageResourceAssociationRequestQuery,
460
+ ]);
461
+
462
+ const handleModalClose = useCallback(() => {
463
+ setActiveModalState({
464
+ modalType: null,
465
+ targetPageId: null,
466
+ defaultParentId: null,
467
+ defaultPathPrefix: "",
468
+ });
469
+ }, []);
470
+
471
+ const handleModalOperationSuccess = useCallback(() => {
472
+ handleModalClose();
473
+ refreshAllPageRelatedData();
474
+ }, [refreshAllPageRelatedData, handleModalClose]);
475
+
476
+ // Calculate new sortOrder when dropping a node
477
+ const calculateNewSortOrder = useCallback(
478
+ (siblings: any[], dropIndex: number, dragNodeId: number): number => {
479
+ // Filter out the dragged node from siblings
480
+ const filteredSiblings = siblings.filter(s => safelyConvertToNumber(s.id) !== dragNodeId);
481
+
482
+ if (filteredSiblings.length === 0) {
483
+ // First item in this parent
484
+ return SORT_ORDER_GAP;
485
+ }
486
+
487
+ if (dropIndex === 0) {
488
+ // Dropped at the beginning
489
+ const firstSibling = filteredSiblings[0];
490
+ const firstOrder = firstSibling?.sortOrder ?? SORT_ORDER_GAP;
491
+ return Math.max(1, Math.floor(firstOrder / 2));
492
+ }
493
+
494
+ if (dropIndex >= filteredSiblings.length) {
495
+ // Dropped at the end
496
+ const lastSibling = filteredSiblings[filteredSiblings.length - 1];
497
+ const lastOrder = lastSibling?.sortOrder ?? 0;
498
+ return lastOrder + SORT_ORDER_GAP;
499
+ }
500
+
501
+ // Dropped in the middle
502
+ const prevSibling = filteredSiblings[dropIndex - 1];
503
+ const nextSibling = filteredSiblings[dropIndex];
504
+ const prevOrder = prevSibling?.sortOrder ?? 0;
505
+ const nextOrder = nextSibling?.sortOrder ?? prevOrder + SORT_ORDER_GAP * 2;
506
+
507
+ // Calculate middle value
508
+ const newOrder = Math.floor((prevOrder + nextOrder) / 2);
509
+
510
+ // If there's no room (gap too small), we need to redistribute
511
+ if (newOrder <= prevOrder || newOrder >= nextOrder) {
512
+ // Return a value that signals we need redistribution
513
+ // For now, just use prevOrder + 1 and let batch save handle redistribution
514
+ return prevOrder + 1;
515
+ }
516
+
517
+ return newOrder;
518
+ },
519
+ [],
520
+ );
521
+
522
+ // Toggle expand/collapse for a node
523
+ const handleToggleExpand = useCallback((pageId: number) => {
524
+ setExpandedKeys(prev => {
525
+ const newSet = new Set(prev);
526
+ if (newSet.has(pageId)) {
527
+ newSet.delete(pageId);
528
+ } else {
529
+ newSet.add(pageId);
530
+ }
531
+ return newSet;
532
+ });
533
+ }, []);
534
+
535
+ // Separate regular pages and virtual pages
536
+ const { regularPagesData, virtualPagesData } = useMemo(() => {
537
+ const pagesArray: any[] = Array.isArray(pageListData?.data) ? pageListData.data : [];
538
+ const regular: any[] = [];
539
+ const virtual: any[] = [];
540
+
541
+ pagesArray.forEach((page: any) => {
542
+ if (page.isVirtual) {
543
+ virtual.push(page);
544
+ } else {
545
+ regular.push(page);
546
+ }
547
+ });
548
+
549
+ return { regularPagesData: regular, virtualPagesData: virtual };
550
+ }, [pageListData]);
551
+
552
+ // Flatten tree data for table display (with depth info for indentation) - only for regular (non-virtual) pages
553
+ const flattenedTableData = useMemo(() => {
554
+ const nodeMapById = new Map<number, any>();
555
+
556
+ // First pass: create all nodes with pending changes applied (only regular pages)
557
+ regularPagesData.forEach((page: any) => {
558
+ const pageId = safelyConvertToNumber(page?.id);
559
+ if (pageId === null) return;
560
+
561
+ const pendingChange = pendingChanges.get(pageId);
562
+ const sortOrder = pendingChange?.sortOrder ?? page.sortOrder;
563
+ const parentId =
564
+ pendingChange?.parentId !== undefined ? pendingChange.parentId : (page.parentId ?? null);
565
+
566
+ nodeMapById.set(pageId, {
567
+ ...page,
568
+ id: pageId,
569
+ sortOrder,
570
+ parentId,
571
+ children: [],
572
+ });
573
+ });
574
+
575
+ // Second pass: build tree structure
576
+ const rootNodes: any[] = [];
577
+ nodeMapById.forEach(node => {
578
+ const parentId = node.parentId;
579
+ if (parentId !== null && nodeMapById.has(parentId)) {
580
+ nodeMapById.get(parentId).children.push(node);
581
+ } else {
582
+ rootNodes.push(node);
583
+ }
584
+ });
585
+
586
+ // Sort children by sortOrder
587
+ const sortNodes = (nodes: any[]) => {
588
+ nodes.sort((a, b) => (a.sortOrder ?? Infinity) - (b.sortOrder ?? Infinity));
589
+ nodes.forEach(node => {
590
+ if (node.children?.length > 0) {
591
+ sortNodes(node.children);
592
+ }
593
+ });
594
+ };
595
+ sortNodes(rootNodes);
596
+
597
+ // Flatten tree to array with depth info (respecting expanded state)
598
+ const result: any[] = [];
599
+ const flatten = (nodes: any[], depth: number, parentExpanded: boolean) => {
600
+ nodes.forEach(node => {
601
+ // Only show node if its parent is expanded (or it's a root node)
602
+ if (!parentExpanded && depth > 0) return;
603
+
604
+ const { children, ...nodeWithoutChildren } = node;
605
+ const hasChildren = children?.length > 0;
606
+ const isExpanded = expandedKeys.has(node.id);
607
+
608
+ result.push({
609
+ ...nodeWithoutChildren,
610
+ _depth: depth,
611
+ _hasChildren: hasChildren,
612
+ _isExpanded: isExpanded,
613
+ _isModified: pendingChanges.has(node.id),
614
+ });
615
+
616
+ if (hasChildren) {
617
+ flatten(children, depth + 1, isExpanded);
618
+ }
619
+ });
620
+ };
621
+ flatten(rootNodes, 0, true);
622
+
623
+ return result;
624
+ }, [regularPagesData, pendingChanges, expandedKeys]);
625
+
626
+ // Get sortable item IDs for dnd-kit
627
+ const sortableIds = useMemo(() => {
628
+ return flattenedTableData.map(item => String(item.id));
629
+ }, [flattenedTableData]);
630
+
631
+ // Live-reordered IDs during drag (for visual feedback)
632
+ const [activeSortableIds, setActiveSortableIds] = useState<string[]>([]);
633
+
634
+ // Sync activeSortableIds when base list changes (e.g. after save/refresh)
635
+ useEffect(() => {
636
+ setActiveSortableIds(sortableIds);
637
+ }, [sortableIds]);
638
+
639
+ // dnd-kit sensors
640
+ const sensors = useSensors(
641
+ useSensor(PointerSensor, {
642
+ activationConstraint: {
643
+ distance: 1,
644
+ },
645
+ }),
646
+ useSensor(KeyboardSensor, {
647
+ coordinateGetter: sortableKeyboardCoordinates,
648
+ }),
649
+ );
650
+
651
+ // Reorder the visual list live during drag for smooth feedback
652
+ const handleDragOver = useCallback((event: DragOverEvent) => {
653
+ const { active, over } = event;
654
+ if (!over || active.id === over.id) return;
655
+ setActiveSortableIds(prev => {
656
+ const oldIndex = prev.indexOf(String(active.id));
657
+ const newIndex = prev.indexOf(String(over.id));
658
+ if (oldIndex === -1 || newIndex === -1) return prev;
659
+ return arrayMove(prev, oldIndex, newIndex);
660
+ });
661
+ }, []);
662
+
663
+ // Handle drag end for dnd-kit
664
+ const handleDragEnd = useCallback(
665
+ (event: DragEndEvent) => {
666
+ const { active, over } = event;
667
+
668
+ if (!over || active.id === over.id) return;
669
+
670
+ const draggedId = safelyConvertToNumber(active.id);
671
+ const overId = safelyConvertToNumber(over.id);
672
+
673
+ if (draggedId === null || overId === null) return;
674
+
675
+ const pagesArray: any[] = Array.isArray(pageListData?.data) ? pageListData.data : [];
676
+ const draggedPage = pagesArray.find(p => safelyConvertToNumber(p.id) === draggedId);
677
+ const overPage = pagesArray.find(p => safelyConvertToNumber(p.id) === overId);
678
+
679
+ if (!draggedPage || !overPage) return;
680
+
681
+ // Get target's parentId (with pending changes applied)
682
+ const overPendingChange = pendingChanges.get(overId);
683
+ const targetParentId =
684
+ overPendingChange?.parentId !== undefined
685
+ ? overPendingChange.parentId
686
+ : (overPage.parentId ?? null);
687
+
688
+ // Get dragged item's current parentId
689
+ const draggedPendingChange = pendingChanges.get(draggedId);
690
+ const currentParentId =
691
+ draggedPendingChange?.parentId !== undefined
692
+ ? draggedPendingChange.parentId
693
+ : (draggedPage.parentId ?? null);
694
+
695
+ // Determine new parentId - when dropping on an item, take its parentId (same level)
696
+ const newParentId = targetParentId;
697
+
698
+ // Get all siblings at the target parent level (with pending changes)
699
+ const getSiblingsWithPendingChanges = (parentId: number | null) => {
700
+ return pagesArray
701
+ .map(page => {
702
+ const pageId = safelyConvertToNumber(page.id);
703
+ if (pageId === null) return null;
704
+ const pending = pendingChanges.get(pageId);
705
+ return {
706
+ ...page,
707
+ id: pageId,
708
+ parentId:
709
+ pending?.parentId !== undefined ? pending.parentId : (page.parentId ?? null),
710
+ sortOrder: pending?.sortOrder ?? page.sortOrder ?? 0,
711
+ };
712
+ })
713
+ .filter(
714
+ (page): page is NonNullable<typeof page> => page !== null && page.parentId === parentId,
715
+ )
716
+ .sort((a, b) => a.sortOrder - b.sortOrder);
717
+ };
718
+
719
+ const siblings = getSiblingsWithPendingChanges(newParentId);
720
+
721
+ // Find the position of the over item in siblings
722
+ const overIndex = siblings.findIndex(s => s.id === overId);
723
+ const dragIndex = siblings.findIndex(s => s.id === draggedId);
724
+
725
+ // Calculate drop index
726
+ let dropIndex: number;
727
+ if (dragIndex === -1) {
728
+ // Dragged item is not in this parent group (moving to new parent)
729
+ dropIndex = overIndex;
730
+ } else if (dragIndex < overIndex) {
731
+ // Moving down
732
+ dropIndex = overIndex;
733
+ } else {
734
+ // Moving up
735
+ dropIndex = overIndex;
736
+ }
737
+
738
+ // Calculate new sort order
739
+ const newSortOrder = calculateNewSortOrder(siblings, dropIndex, draggedId);
740
+
741
+ // Check if anything changed
742
+ const parentChanged = newParentId !== currentParentId;
743
+ const orderChanged =
744
+ newSortOrder !== (draggedPendingChange?.sortOrder ?? draggedPage.sortOrder ?? 0);
745
+
746
+ if (!parentChanged && !orderChanged) return;
747
+
748
+ // Update pending changes
749
+ setPendingChanges(prev => {
750
+ const newChanges = new Map(prev);
751
+ const existingChange = newChanges.get(draggedId) || {};
752
+
753
+ newChanges.set(draggedId, {
754
+ ...existingChange,
755
+ ...(parentChanged && { parentId: newParentId }),
756
+ sortOrder: newSortOrder,
757
+ });
758
+
759
+ return newChanges;
760
+ });
761
+
762
+ message.info(`Page "${draggedPage.name}" moved. Click "Save Order" to apply changes.`);
763
+ },
764
+ [pageListData, pendingChanges, calculateNewSortOrder],
765
+ );
766
+
767
+ // Discard all pending changes
768
+ const handleDiscardChanges = useCallback(() => {
769
+ if (pendingChanges.size === 0) return;
770
+
771
+ Modal.confirm({
772
+ title: "Discard Changes",
773
+ content: `Are you sure you want to discard ${pendingChanges.size} pending change(s)?`,
774
+ okText: "Discard",
775
+ okType: "danger",
776
+ styles: { body: { padding: "16px 24px" } },
777
+ onOk: () => {
778
+ setPendingChanges(new Map());
779
+ message.success("Changes discarded");
780
+ },
781
+ });
782
+ }, [pendingChanges]);
783
+
784
+ // Save all pending changes
785
+ const handleSaveOrder = useCallback(async () => {
786
+ if (pendingChanges.size === 0) {
787
+ message.info("No changes to save");
788
+ return;
789
+ }
790
+
791
+ setIsSavingOrder(true);
792
+
793
+ try {
794
+ const updatePromises: Promise<any>[] = [];
795
+
796
+ pendingChanges.forEach((change, pageId) => {
797
+ const updateData: { sortOrder?: number; parentId?: number | null } = {};
798
+
799
+ if (change.sortOrder !== undefined) {
800
+ updateData.sortOrder = change.sortOrder;
801
+ }
802
+ if (change.parentId !== undefined) {
803
+ updateData.parentId = change.parentId;
804
+ }
805
+
806
+ if (Object.keys(updateData).length > 0) {
807
+ updatePromises.push(updatePageOrder(pageId, updateData));
808
+ }
809
+ });
810
+
811
+ await Promise.all(updatePromises);
812
+
813
+ message.success(`Successfully updated ${pendingChanges.size} page(s)`);
814
+ setPendingChanges(new Map());
815
+ refreshAllPageRelatedData();
816
+ } catch (error) {
817
+ console.error("Failed to save order:", error);
818
+ message.error("Failed to save some changes. Please try again.");
819
+ } finally {
820
+ setIsSavingOrder(false);
821
+ }
822
+ }, [pendingChanges, refreshAllPageRelatedData]);
823
+
824
+ // Build parent-children lookup for expand operations
825
+ const parentChildrenMap = useMemo(() => {
826
+ const childrenOf = new Map<number | null, number[]>(); // parentId -> childIds
827
+ for (const page of regularPagesData) {
828
+ const pageId = safelyConvertToNumber(page?.id);
829
+ if (pageId === null) continue;
830
+ const pendingChange = pendingChanges.get(pageId);
831
+ const parentId =
832
+ pendingChange?.parentId !== undefined ? pendingChange.parentId : (page.parentId ?? null);
833
+ if (!childrenOf.has(parentId)) childrenOf.set(parentId, []);
834
+ childrenOf.get(parentId)!.push(pageId);
835
+ }
836
+ return childrenOf;
837
+ }, [regularPagesData, pendingChanges]);
838
+
839
+ // Expand to N levels (single click = 2 levels)
840
+ const handleExpandLevels = useCallback(
841
+ (maxDepth: number) => {
842
+ const keysToExpand = new Set<number>();
843
+ const expand = (parentId: number | null, depth: number) => {
844
+ if (depth >= maxDepth) return;
845
+ const children = parentChildrenMap.get(parentId);
846
+ if (!children) return;
847
+ for (const childId of children) {
848
+ // Only expand nodes that have children
849
+ if (parentChildrenMap.has(childId)) {
850
+ keysToExpand.add(childId);
851
+ expand(childId, depth + 1);
852
+ }
853
+ }
854
+ };
855
+ expand(null, 0);
856
+ setExpandedKeys(keysToExpand);
857
+ },
858
+ [parentChildrenMap],
859
+ );
860
+
861
+ // Expand all nodes (double click)
862
+ const handleExpandAll = useCallback(() => {
863
+ // Expand every node that has children
864
+ const keysToExpand = new Set<number>();
865
+ for (const [parentId] of parentChildrenMap) {
866
+ if (parentId !== null) keysToExpand.add(parentId);
867
+ }
868
+ setExpandedKeys(keysToExpand);
869
+ }, [parentChildrenMap]);
870
+
871
+ // Collapse all nodes
872
+ const handleCollapseAll = useCallback(() => {
873
+ setExpandedKeys(new Set());
874
+ }, []);
875
+
876
+ // Common column renderers for both tables
877
+ const renderAssignedResourcesColumn = (record: any) => {
878
+ const permissionSummary = pageResourcePermissionSummaryMap[record.id];
879
+
880
+ if (!permissionSummary?.resourceNames.length) {
881
+ return <span style={{ color: "#999" }}>No resources assigned</span>;
882
+ }
883
+
884
+ const joinedResourceNames = permissionSummary.resourceNames.join(", ");
885
+ const shouldTruncateDisplay =
886
+ joinedResourceNames.length > RESOURCE_NAME_DISPLAY_TRUNCATION_LIMIT;
887
+ const truncatedDisplayValue = shouldTruncateDisplay
888
+ ? `${joinedResourceNames.slice(0, RESOURCE_NAME_DISPLAY_TRUNCATION_LIMIT)}...`
889
+ : joinedResourceNames;
890
+
891
+ return (
892
+ <Tooltip title={joinedResourceNames}>
893
+ <span>{truncatedDisplayValue}</span>
894
+ </Tooltip>
895
+ );
896
+ };
897
+
898
+ const renderPermissionActionsColumn = (record: any) => {
899
+ const permissionSummary = pageResourcePermissionSummaryMap[record.id];
900
+
901
+ if (!permissionSummary?.allPermissionActions.length) {
902
+ return <span style={{ color: "#999" }}>-</span>;
903
+ }
904
+
905
+ return (
906
+ <Space size={[4, 4]} wrap>
907
+ {permissionSummary.allPermissionActions.map(permissionAction => (
908
+ <Tag key={`${record.id}-${permissionAction}`} color="blue">
909
+ {permissionAction}
910
+ </Tag>
911
+ ))}
912
+ </Space>
913
+ );
914
+ };
915
+
916
+ // Handle click on add child page button
917
+ const handleAddChildPage = useCallback((parentPage: any) => {
918
+ const parentPath = parentPage.path || "";
919
+ const pathPrefix = parentPath.endsWith("/") ? parentPath : `${parentPath}/`;
920
+ setActiveModalState({
921
+ modalType: "create",
922
+ targetPageId: null,
923
+ defaultParentId: parentPage.id,
924
+ defaultPathPrefix: pathPrefix,
925
+ });
926
+ }, []);
927
+
928
+ const renderActionsColumn = (record: any, hasChildren: boolean = false) => (
929
+ <Space>
930
+ <Tooltip title="Add child page">
931
+ <Button
932
+ type="default"
933
+ size="small"
934
+ icon={<PlusOutlined />}
935
+ onClick={() => handleAddChildPage(record)}
936
+ />
937
+ </Tooltip>
938
+ <EditButton
939
+ hideText
940
+ size="small"
941
+ recordItemId={record.id}
942
+ onClick={() => setActiveModalState({ modalType: "edit", targetPageId: record.id })}
943
+ />
944
+ <Tooltip title="Generate SQL">
945
+ <Button
946
+ type="default"
947
+ size="small"
948
+ icon={<CodeOutlined />}
949
+ onClick={() => handleShowSql(record, hasChildren)}
950
+ />
951
+ </Tooltip>
952
+ {/* Only show delete button for leaf nodes (pages without children) */}
953
+ {!hasChildren && (
954
+ <>
955
+ <Tooltip title="Assign roles">
956
+ <Button
957
+ type="default"
958
+ size="small"
959
+ icon={<TeamOutlined />}
960
+ onClick={() => setAssignRolesTarget({ pageId: record.id, pageName: record.name })}
961
+ />
962
+ </Tooltip>
963
+ <Tooltip title="View details">
964
+ <Button
965
+ type="default"
966
+ size="small"
967
+ icon={<EyeOutlined />}
968
+ onClick={() => setDrawerPage(record)}
969
+ />
970
+ </Tooltip>
971
+ <DeleteButton
972
+ hideText
973
+ size="small"
974
+ recordItemId={record.id}
975
+ resource="page"
976
+ onSuccess={() => refreshAllPageRelatedData()}
977
+ />
978
+ </>
979
+ )}
980
+ </Space>
981
+ );
982
+
983
+ // Render regular pages table with drag-drop
984
+ const renderRegularPagesTable = () => (
985
+ <>
986
+ <Space style={{ marginBottom: 16 }}>
987
+ <Tooltip title="Click: expand 2 levels / Double-click: expand all">
988
+ <Button
989
+ onClick={() => {
990
+ expandClickTimer.current = window.setTimeout(() => handleExpandLevels(1), 200);
991
+ }}
992
+ onDoubleClick={() => {
993
+ if (expandClickTimer.current) clearTimeout(expandClickTimer.current);
994
+ handleExpandAll();
995
+ }}
996
+ >
997
+ Expand All
998
+ </Button>
999
+ </Tooltip>
1000
+ <Button onClick={handleCollapseAll}>Collapse All</Button>
1001
+ {pendingChanges.size > 0 && (
1002
+ <>
1003
+ <Button
1004
+ type="primary"
1005
+ icon={<SaveOutlined />}
1006
+ onClick={handleSaveOrder}
1007
+ loading={isSavingOrder}
1008
+ >
1009
+ Save Order ({pendingChanges.size} change{pendingChanges.size > 1 ? "s" : ""})
1010
+ </Button>
1011
+ <Button icon={<UndoOutlined />} onClick={handleDiscardChanges} disabled={isSavingOrder}>
1012
+ Discard Changes
1013
+ </Button>
1014
+ </>
1015
+ )}
1016
+ </Space>
1017
+
1018
+ {/* Info alert about drag and drop */}
1019
+ <Alert
1020
+ message="Drag and drop rows using the handle (⋮⋮) to reorder pages. Changes are saved in batch when you click 'Save Order'."
1021
+ type="info"
1022
+ showIcon
1023
+ style={{ marginBottom: 16 }}
1024
+ closable
1025
+ />
1026
+
1027
+ {/* Table with dnd-kit drag and drop */}
1028
+ <DndContext
1029
+ sensors={sensors}
1030
+ collisionDetection={closestCenter}
1031
+ modifiers={[restrictToVerticalAxis]}
1032
+ onDragOver={handleDragOver}
1033
+ onDragEnd={handleDragEnd}
1034
+ >
1035
+ <SortableContext items={activeSortableIds} strategy={verticalListSortingStrategy}>
1036
+ <Table
1037
+ {...tableProps}
1038
+ rowKey="id"
1039
+ dataSource={flattenedTableData}
1040
+ pagination={false}
1041
+ components={{
1042
+ body: {
1043
+ row: DraggableRow,
1044
+ },
1045
+ }}
1046
+ >
1047
+ {/* Drag handle column */}
1048
+ <Table.Column key="drag-handle" width={50} render={() => null} />
1049
+ <Table.Column
1050
+ dataIndex="name"
1051
+ title="Page Name"
1052
+ render={(text, record: any) => (
1053
+ <span
1054
+ style={{
1055
+ paddingLeft: record._depth * 20,
1056
+ display: "inline-flex",
1057
+ alignItems: "center",
1058
+ }}
1059
+ >
1060
+ {record._hasChildren ? (
1061
+ <span
1062
+ onClick={e => {
1063
+ e.stopPropagation();
1064
+ handleToggleExpand(record.id);
1065
+ }}
1066
+ style={{
1067
+ cursor: "pointer",
1068
+ marginRight: 8,
1069
+ display: "inline-flex",
1070
+ alignItems: "center",
1071
+ }}
1072
+ >
1073
+ {record._isExpanded ? (
1074
+ <DownOutlined style={{ fontSize: 10 }} />
1075
+ ) : (
1076
+ <RightOutlined style={{ fontSize: 10 }} />
1077
+ )}
1078
+ </span>
1079
+ ) : (
1080
+ <span style={{ width: 18 }} />
1081
+ )}
1082
+ {i18n.language === "zh_CN" ? record.zhName || text : record.enName || text}
1083
+ {record._isModified && (
1084
+ <Tag color="orange" style={{ marginLeft: 8, fontSize: 10 }}>
1085
+ Modified
1086
+ </Tag>
1087
+ )}
1088
+ </span>
1089
+ )}
1090
+ />
1091
+ <Table.Column dataIndex="path" title="Page Path" />
1092
+ <Table.Column
1093
+ dataIndex="description"
1094
+ title="Description"
1095
+ ellipsis={{ showTitle: true }}
1096
+ onCell={() => ({ style: { maxWidth: 200 } })}
1097
+ />
1098
+ <Table.Column dataIndex="creator" title="Creator" />
1099
+ <Table.Column
1100
+ title="Actions"
1101
+ render={(_, record: any) => renderActionsColumn(record, record._hasChildren)}
1102
+ />
1103
+ </Table>
1104
+ </SortableContext>
1105
+ </DndContext>
1106
+ </>
1107
+ );
1108
+
1109
+ // Render virtual pages table (simple table without drag-drop and tree structure)
1110
+ const renderVirtualPagesTable = () => (
1111
+ <Table rowKey="id" dataSource={virtualPagesData} pagination={{ pageSize: 20 }}>
1112
+ <Table.Column dataIndex="name" title="Page Name" />
1113
+ <Table.Column dataIndex="creator" title="Creator" />
1114
+ <Table.Column
1115
+ dataIndex="description"
1116
+ title="Description"
1117
+ ellipsis={{ showTitle: true }}
1118
+ onCell={() => ({ style: { maxWidth: 200 } })}
1119
+ />
1120
+ <Table.Column
1121
+ title="Actions"
1122
+ render={(_, record: any) => renderActionsColumn(record, false)}
1123
+ />
1124
+ </Table>
1125
+ );
1126
+
1127
+ return (
1128
+ <div>
1129
+ <Space style={{ marginBottom: 16 }}>
1130
+ <Button
1131
+ type="primary"
1132
+ onClick={() =>
1133
+ setActiveModalState({
1134
+ modalType: "create",
1135
+ targetPageId: null,
1136
+ })
1137
+ }
1138
+ >
1139
+ Create Page
1140
+ </Button>
1141
+ </Space>
1142
+
1143
+ <Tabs
1144
+ activeKey={activeTab}
1145
+ onChange={setActiveTab}
1146
+ items={[
1147
+ {
1148
+ key: "regular",
1149
+ label: `Regular Pages (${regularPagesData.length})`,
1150
+ children: renderRegularPagesTable(),
1151
+ },
1152
+ {
1153
+ key: "virtual",
1154
+ label: `Virtual Pages (${virtualPagesData.length})`,
1155
+ children: renderVirtualPagesTable(),
1156
+ },
1157
+ ]}
1158
+ />
1159
+
1160
+ {activeModalState.modalType === "edit" && activeModalState.targetPageId && (
1161
+ <EditPageModal
1162
+ pageIdToEdit={activeModalState.targetPageId}
1163
+ onModalClose={handleModalClose}
1164
+ onPageEditSuccess={handleModalOperationSuccess}
1165
+ parentPageTreeOptions={pageMenuTreeOptionsData}
1166
+ allAvailableResources={availableResourcesListForModal}
1167
+ currentPageResourceAssignments={getPageResourceAssignmentsForModal(
1168
+ activeModalState.targetPageId,
1169
+ )}
1170
+ allPagesData={pageListData?.data || []}
1171
+ />
1172
+ )}
1173
+
1174
+ {drawerPage && (
1175
+ <PageDetailDrawer
1176
+ page={drawerPage}
1177
+ open={!!drawerPage}
1178
+ onClose={() => setDrawerPage(null)}
1179
+ />
1180
+ )}
1181
+
1182
+ {assignRolesTarget && (
1183
+ <AssignRolesModal
1184
+ pageId={assignRolesTarget.pageId}
1185
+ pageName={assignRolesTarget.pageName}
1186
+ open={!!assignRolesTarget}
1187
+ onClose={() => setAssignRolesTarget(null)}
1188
+ onSuccess={() => {
1189
+ setAssignRolesTarget(null);
1190
+ refreshAllPageRelatedData();
1191
+ }}
1192
+ />
1193
+ )}
1194
+
1195
+ {activeModalState.modalType === "create" && (
1196
+ <CreatePageModal
1197
+ onModalClose={handleModalClose}
1198
+ onPageCreationSuccess={handleModalOperationSuccess}
1199
+ parentPageTreeOptions={pageMenuTreeOptionsData}
1200
+ allAvailableResources={availableResourcesListForModal}
1201
+ allPagesData={pageListData?.data || []}
1202
+ defaultParentId={activeModalState.defaultParentId}
1203
+ defaultPathPrefix={activeModalState.defaultPathPrefix}
1204
+ />
1205
+ )}
1206
+
1207
+ <SqlModal
1208
+ open={sqlModal.open}
1209
+ onClose={() => setSqlModal({ open: false, title: "", sql: "" })}
1210
+ title={sqlModal.title}
1211
+ sql={sqlModal.sql}
1212
+ />
1213
+ </div>
1214
+ );
1215
+ };