@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.
- package/dist/lib/application/files/gadmin2-game-angle-demo/.dockerignore +2 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/Dockerfile +40 -26
- package/dist/lib/application/files/gadmin2-game-angle-demo/Jenkinsfile +30 -4
- package/dist/lib/application/files/gadmin2-game-angle-demo/config/prisma/example.prisma +33 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/config/prisma/system.prisma +170 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/config/ui/Event.ts +70 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/config/ui/Game.ts +6 -6
- package/dist/lib/application/files/gadmin2-game-angle-demo/config/ui/ITActivityDay.ts +70 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/config/ui/Log.ts +2 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/config/ui/Role.ts +2 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/.env +20 -9
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/.env.local +1 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/.eslintrc.js +1 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/.prettierignore +1 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/README.md +2 -18
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/gadmin-cli.json +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/migrate-between-pg-schemas.js +1232 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/package.json +65 -38
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/prisma/.generator.prisma +4 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/prisma.config.ts +16 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/games.ts +1 -71
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/index.ts +17 -21
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/permissions.ts +278 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/seedDataMngtPages.ts +258 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/users.ts +7 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/alias.config.ts +7 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/app.controller.ts +151 -11
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/app.module.ts +29 -13
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/app.service.ts +151 -12
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/lib/auth.guard.ts +87 -41
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/lib/http-cache.interceptor.ts +21 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/lib/logger.ts +19 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/lib/safe-log.util.ts +176 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/lib/{yufuid.ts → taihu.ts} +49 -34
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/lib/tracing.ts +174 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/lib/trim.pipe.ts +51 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/lib/utils.ts +91 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/lib/woaAuth.ts +25 -12
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/main.ts +22 -12
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/audit/audit.controller.spec.ts +20 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/audit/audit.controller.ts +190 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/audit/audit.module.ts +10 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/audit/audit.service.spec.ts +338 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/audit/audit.service.ts +83 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/game/game.controller.spec.ts +20 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/game/game.controller.ts +188 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/game/game.module.ts +10 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/game/game.service.spec.ts +18 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/game/game.service.ts +83 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/page/page.controller.spec.ts +20 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/page/page.controller.ts +250 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/page/page.module.ts +10 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/page/page.service.spec.ts +18 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/page/page.service.ts +1051 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/pageResource/pageResource.controller.spec.ts +20 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/pageResource/pageResource.controller.ts +196 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/pageResource/pageResource.module.ts +13 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/pageResource/pageResource.service.spec.ts +18 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/pageResource/pageResource.service.ts +219 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/resource/resource.controller.spec.ts +20 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/resource/resource.controller.ts +196 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/resource/resource.module.ts +10 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/resource/resource.service.spec.ts +18 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/resource/resource.service.ts +199 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/role/role.controller.spec.ts +20 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/role/role.controller.ts +210 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/role/role.module.ts +12 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/role/role.service.spec.ts +18 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/role/role.service.ts +849 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/role/roles-refresher.service.ts +133 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/rolePages/rolePages.controller.spec.ts +20 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/rolePages/rolePages.controller.ts +196 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/rolePages/rolePages.module.ts +10 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/rolePages/rolePages.service.spec.ts +18 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/rolePages/rolePages.service.ts +201 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/roleResource/roleResource.controller.spec.ts +20 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/roleResource/roleResource.controller.ts +196 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/roleResource/roleResource.module.ts +10 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/roleResource/roleResource.service.spec.ts +18 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/roleResource/roleResource.service.ts +216 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.controller.spec.ts +20 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.controller.ts +198 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.module.ts +10 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.service.spec.ts +18 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.service.ts +104 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/start-prod.sh +130 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/tsconfig.json +18 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/index.html +19 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/package.json +34 -42
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/postcss.config.cjs +6 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/App.tsx +111 -185
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/auditLogProvider.ts +5 -5
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/authProvider.ts +2 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/SqlModal.tsx +419 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/contexts/business/index.tsx +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/contexts/color-mode/index.tsx +49 -51
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/custom-avatar.tsx +38 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/index.ts +4 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/layout/{header/index.tsx → header.tsx} +22 -31
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/layout/index.ts +3 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/layout/layout.tsx +32 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/layout/logo.tsx +19 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/layout/sider.tsx +331 -166
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/layout/title.tsx +61 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/pagination-total.tsx +21 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/tags/index.ts +1 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/tags/role-tag.tsx +44 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/text.tsx +74 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/config/http.ts +28 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/config/routeRegistry.tsx +258 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/constants/layout.ts +16 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/enums/audit-log.enum.ts +13 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/enums/index.ts +1 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/helpers/get-name-initials.ts +8 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/helpers/get-random-color.ts +27 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/helpers/http.ts +87 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/helpers/index.tsx +6 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/helpers/login.ts +22 -59
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/helpers/utils.tsx +5 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/hooks/useDynamicResources.tsx +211 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/hooks/useFetchData.ts +33 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/hooks/useRoles.ts +30 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/hooks/useUserPageAccess.ts +339 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/i18n.ts +8 -4
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/index.tsx +3 -11
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/{public → src}/locales/en/common.json +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/{public → src}/locales/zh_CN/common.json +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/audit/components/action-cell.css +3 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/audit/components/action-cell.tsx +134 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/audit/create.tsx +113 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/audit/edit.tsx +122 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/audit/index.ts +8 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/audit/index.tsx +6 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/audit/list.tsx +213 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/audit/show.tsx +61 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/page/Components/AssignRolesModal.tsx +168 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/page/Components/CreatePageModal.tsx +42 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/page/Components/EditPageModal.tsx +42 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/page/Components/PageDetailDrawer.tsx +101 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/page/Components/PageFormModal.tsx +731 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/page/hooks/usePageManagement.ts +36 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/page/index.ts +1 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/page/list.tsx +1215 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/page/queries.ts +17 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/page/types.ts +45 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/permissionReadme/index.tsx +1089 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/resource/Components/CreateModal.tsx +25 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/resource/Components/EditModal.tsx +28 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/resource/Components/ResourceDetailDrawer.tsx +160 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/resource/Components/modal.tsx +202 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/resource/index.ts +2 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/resource/list.tsx +212 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/resource/queries.ts +10 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/resource/types.ts +9 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/role/Components/CreateModal.tsx +30 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/role/Components/EditModal.tsx +47 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/role/Components/RoleDetailDrawer.tsx +56 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/role/Components/modal.tsx +302 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/role/hooks/useRolePage.ts +35 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/role/index.ts +1 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/role/list.tsx +431 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/role/queries.ts +8 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/role/types.ts +8 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/user/components/create-modal.tsx +17 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/user/components/edit-modal.tsx +19 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/user/components/form-modal.tsx +188 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/user/components/index.ts +5 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/user/components/role-tag.tsx +48 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/user/components/show-drawer.tsx +140 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/user/index.ts +1 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/user/list.tsx +372 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/user/queries.ts +14 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/styles/antd.css +132 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/styles/fc.css +58 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/styles/index.css +128 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/styles/show-drawer.module.css +18 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/styles/show-page.module.css +21 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/types/audit-log.ts +1 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/types/index.ts +3 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/types/role.ts +7 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/types/user.ts +1 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/vite-env.d.ts +1 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/tsconfig.json +5 -4
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/vite.config.ts +31 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/yarn.lock +8321 -0
- package/package.json +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/config/prisma/sample.prisma +0 -65
- package/dist/lib/application/files/gadmin2-game-angle-demo/config/ui/Source.ts +0 -76
- package/dist/lib/application/files/gadmin2-game-angle-demo/config/ui/Tasklog.ts +0 -76
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/roles.ts +0 -4
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/craco.config.js +0 -27
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/public/index.html +0 -53
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/layout/styles.ts +0 -10
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/helpers/utils.ts +0 -76
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/react-app-env.d.ts +0 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/reportWebVitals.ts +0 -15
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/styles/antd.less +0 -79
- /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
|
+
};
|