From 9fef47f29169e371fbbedd04534ae19c87c8a352 Mon Sep 17 00:00:00 2001 From: Ryan Wang Date: Fri, 12 Aug 2022 16:20:21 +0800 Subject: [PATCH] feat: menus management (halo-dev/console#595) Signed-off-by: Ryan Wang --- package.json | 4 +- packages/shared/package.json | 2 +- packages/shared/src/utils/api-client.ts | 8 +- pnpm-lock.yaml | 51 ++- src/modules/interface/menus/MenuList.vue | 175 -------- src/modules/interface/menus/Menus.vue | 194 +++++++++ .../menus/components/MenuEditingModal.vue | 122 ++++++ .../menus/components/MenuItemEditingModal.vue | 137 +++++++ .../menus/components/MenuItemListItem.vue | 127 ++++++ .../interface/menus/components/MenuList.vue | 184 +++++++++ src/modules/interface/menus/module.ts | 4 +- .../__snapshots__/index.spec.ts.snap | 378 ++++++++++++++++++ .../menus/utils/__tests__/index.spec.ts | 269 +++++++++++++ src/modules/interface/menus/utils/index.ts | 267 +++++++++++++ 14 files changed, 1733 insertions(+), 189 deletions(-) delete mode 100644 src/modules/interface/menus/MenuList.vue create mode 100644 src/modules/interface/menus/Menus.vue create mode 100644 src/modules/interface/menus/components/MenuEditingModal.vue create mode 100644 src/modules/interface/menus/components/MenuItemEditingModal.vue create mode 100644 src/modules/interface/menus/components/MenuItemListItem.vue create mode 100644 src/modules/interface/menus/components/MenuList.vue create mode 100644 src/modules/interface/menus/utils/__tests__/__snapshots__/index.spec.ts.snap create mode 100644 src/modules/interface/menus/utils/__tests__/index.spec.ts create mode 100644 src/modules/interface/menus/utils/index.ts diff --git a/package.json b/package.json index d5fe5f027..b009b5361 100644 --- a/package.json +++ b/package.json @@ -34,11 +34,12 @@ "@formkit/vue": "1.0.0-beta.10", "@halo-dev/admin-api": "^1.1.0", "@halo-dev/admin-shared": "workspace:*", - "@halo-dev/api-client": "^0.0.11", + "@halo-dev/api-client": "^0.0.10", "@halo-dev/components": "workspace:*", "@halo-dev/richtext-editor": "0.0.0-alpha.1", "@vueuse/components": "^8.9.4", "@vueuse/core": "^8.9.4", + "@vueuse/router": "^9.1.0", "axios": "^0.27.2", "filepond": "^4.30.4", "filepond-plugin-image-preview": "^4.6.11", @@ -52,6 +53,7 @@ "vue-filepond": "^7.0.3", "vue-grid-layout": "3.0.0-beta1", "vue-router": "^4.1.3", + "vuedraggable": "^4.1.0", "yaml": "^2.1.1" }, "devDependencies": { diff --git a/packages/shared/package.json b/packages/shared/package.json index cdf23287e..98b84a6cd 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -36,7 +36,7 @@ "homepage": "https://github.com/halo-dev/halo-admin/tree/next/shared/components#readme", "license": "MIT", "dependencies": { - "@halo-dev/api-client": "^0.0.11", + "@halo-dev/api-client": "^0.0.10", "@halo-dev/components": "workspace:*", "axios": "^0.27.2" }, diff --git a/packages/shared/src/utils/api-client.ts b/packages/shared/src/utils/api-client.ts index b6b04b6df..f195c3de3 100644 --- a/packages/shared/src/utils/api-client.ts +++ b/packages/shared/src/utils/api-client.ts @@ -9,6 +9,8 @@ import { V1alpha1RoleBindingApi, V1alpha1SettingApi, V1alpha1UserApi, + V1alpha1MenuApi, + V1alpha1MenuItemApi, ThemeHaloRunV1alpha1ThemeApi, ApiHaloRunV1alpha1ThemeApi, } from "@halo-dev/api-client"; @@ -61,10 +63,8 @@ function setupApiClient(axios: AxiosInstance) { plugin: new PluginHaloRunV1alpha1PluginApi(undefined, apiUrl, axios), user: new V1alpha1UserApi(undefined, apiUrl, axios), theme: new ThemeHaloRunV1alpha1ThemeApi(undefined, apiUrl, axios), - - // TODO optional - // link: new CoreHaloRunV1alpha1LinkApi(undefined, apiUrl, axios), - // linkGroup: new CoreHaloRunV1alpha1LinkGroupApi(undefined, apiUrl, axios), + menu: new V1alpha1MenuApi(undefined, apiUrl, axios), + menuItem: new V1alpha1MenuItemApi(undefined, apiUrl, axios), }, user: new ApiHaloRunV1alpha1UserApi(undefined, apiUrl, axios), plugin: new ApiHaloRunV1alpha1PluginApi(undefined, apiUrl, axios), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8aabf9120..092767fbc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,7 +14,7 @@ importers: '@formkit/vue': 1.0.0-beta.10 '@halo-dev/admin-api': ^1.1.0 '@halo-dev/admin-shared': workspace:* - '@halo-dev/api-client': ^0.0.11 + '@halo-dev/api-client': ^0.0.10 '@halo-dev/components': workspace:* '@halo-dev/richtext-editor': 0.0.0-alpha.1 '@rushstack/eslint-patch': ^1.1.4 @@ -35,6 +35,7 @@ importers: '@vue/tsconfig': ^0.1.3 '@vueuse/components': ^8.9.4 '@vueuse/core': ^8.9.4 + '@vueuse/router': ^9.1.0 autoprefixer: ^10.4.8 axios: ^0.27.2 c8: ^7.12.0 @@ -74,6 +75,7 @@ importers: vue-grid-layout: 3.0.0-beta1 vue-router: ^4.1.3 vue-tsc: ^0.39.5 + vuedraggable: ^4.1.0 yaml: ^2.1.1 dependencies: '@formkit/addons': 1.0.0-beta.10_vue@3.2.37 @@ -85,11 +87,12 @@ importers: '@formkit/vue': 1.0.0-beta.10_wwmyxdjqen5bmh3tr2meig5lki '@halo-dev/admin-api': 1.1.0 '@halo-dev/admin-shared': link:packages/shared - '@halo-dev/api-client': 0.0.11 + '@halo-dev/api-client': 0.0.10 '@halo-dev/components': link:packages/components '@halo-dev/richtext-editor': 0.0.0-alpha.1_vue@3.2.37 '@vueuse/components': 8.9.4_vue@3.2.37 '@vueuse/core': 8.9.4_vue@3.2.37 + '@vueuse/router': 9.1.0_45jhv7el6g2ztux7wgm2ofrf4e axios: 0.27.2 filepond: 4.30.4 filepond-plugin-image-preview: 4.6.11_filepond@4.30.4 @@ -103,6 +106,7 @@ importers: vue-filepond: 7.0.3_filepond@4.30.4+vue@3.2.37 vue-grid-layout: 3.0.0-beta1 vue-router: 4.1.3_vue@3.2.37 + vuedraggable: 4.1.0_vue@3.2.37 yaml: 2.1.1 devDependencies: '@changesets/cli': 2.24.2 @@ -176,12 +180,12 @@ importers: packages/shared: specifiers: - '@halo-dev/api-client': ^0.0.11 + '@halo-dev/api-client': ^0.0.10 '@halo-dev/components': workspace:* axios: ^0.27.2 vite-plugin-dts: ^1.4.1 dependencies: - '@halo-dev/api-client': 0.0.11 + '@halo-dev/api-client': 0.0.10 '@halo-dev/components': link:../components axios: 0.27.2 devDependencies: @@ -2126,8 +2130,8 @@ packages: - debug dev: false - /@halo-dev/api-client/0.0.11: - resolution: {integrity: sha512-67e7lrWPoHVKF4XItT6OWr7rX96CR9s/3SOVXYD3xCyAMIgLyFULfKVI7JGxisFMKvadTx7rm5RK4zzZ/CbXIw==} + /@halo-dev/api-client/0.0.10: + resolution: {integrity: sha512-DKQKkEAKMR/rbopI6jbjbzLiYUZeY6dOcgqGoDGG8MAcwkWOI6iWaZnuR5z+X8vd51XjiPhnekAphfcO6PaWEQ==} dev: false /@halo-dev/logger/1.1.0: @@ -3653,6 +3657,19 @@ packages: resolution: {integrity: sha512-IwSfzH80bnJMzqhaapqJl9JRIiyQU0zsRGEgnxN6jhq7992cPUJIRfV+JHRIZXjYqbwt07E1gTEp0R0zPJ1aqw==} dev: false + /@vueuse/router/9.1.0_45jhv7el6g2ztux7wgm2ofrf4e: + resolution: {integrity: sha512-ub2B8XfHCQXX4Migab9bBnFjJI+NmqeLbQox1UdhWhy9cZIpgKx40aW7eq/LLUMGVIKAdriDI6YsD0LZrZFeaA==} + peerDependencies: + vue-router: '>=4.0.0-rc.1' + dependencies: + '@vueuse/shared': 9.1.0_vue@3.2.37 + vue-demi: 0.12.1_vue@3.2.37 + vue-router: 4.1.3_vue@3.2.37 + transitivePeerDependencies: + - '@vue/composition-api' + - vue + dev: false + /@vueuse/shared/8.9.4_vue@3.2.37: resolution: {integrity: sha512-wt+T30c4K6dGRMVqPddexEVLa28YwxW5OFIPmzUHICjphfAuBFTTdDoyqREZNDOFJZ44ARH1WWQNCUK8koJ+Ag==} peerDependencies: @@ -3668,6 +3685,15 @@ packages: vue-demi: 0.12.1_vue@3.2.37 dev: false + /@vueuse/shared/9.1.0_vue@3.2.37: + resolution: {integrity: sha512-pB/3njQu4tfJJ78ajELNda0yMG6lKfpToQW7Soe09CprF1k3QuyoNi1tBNvo75wBDJWD+LOnr+c4B5HZ39jY/Q==} + dependencies: + vue-demi: 0.12.1_vue@3.2.37 + transitivePeerDependencies: + - '@vue/composition-api' + - vue + dev: false + /abab/2.0.6: resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} dev: true @@ -8195,6 +8221,10 @@ packages: yargs: 15.4.1 dev: true + /sortablejs/1.14.0: + resolution: {integrity: sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==} + dev: false + /source-map-js/1.0.2: resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} engines: {node: '>=0.10.0'} @@ -9279,6 +9309,15 @@ packages: '@vue/server-renderer': 3.2.37_vue@3.2.37 '@vue/shared': 3.2.37 + /vuedraggable/4.1.0_vue@3.2.37: + resolution: {integrity: sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==} + peerDependencies: + vue: ^3.0.1 + dependencies: + sortablejs: 1.14.0 + vue: 3.2.37 + dev: false + /w3c-hr-time/1.0.2: resolution: {integrity: sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==} dependencies: diff --git a/src/modules/interface/menus/MenuList.vue b/src/modules/interface/menus/MenuList.vue deleted file mode 100644 index 94ae72333..000000000 --- a/src/modules/interface/menus/MenuList.vue +++ /dev/null @@ -1,175 +0,0 @@ - - diff --git a/src/modules/interface/menus/Menus.vue b/src/modules/interface/menus/Menus.vue new file mode 100644 index 000000000..97447e9f2 --- /dev/null +++ b/src/modules/interface/menus/Menus.vue @@ -0,0 +1,194 @@ + + diff --git a/src/modules/interface/menus/components/MenuEditingModal.vue b/src/modules/interface/menus/components/MenuEditingModal.vue new file mode 100644 index 000000000..05424be23 --- /dev/null +++ b/src/modules/interface/menus/components/MenuEditingModal.vue @@ -0,0 +1,122 @@ + + diff --git a/src/modules/interface/menus/components/MenuItemEditingModal.vue b/src/modules/interface/menus/components/MenuItemEditingModal.vue new file mode 100644 index 000000000..13eb7a17c --- /dev/null +++ b/src/modules/interface/menus/components/MenuItemEditingModal.vue @@ -0,0 +1,137 @@ + + diff --git a/src/modules/interface/menus/components/MenuItemListItem.vue b/src/modules/interface/menus/components/MenuItemListItem.vue new file mode 100644 index 000000000..96a4cf173 --- /dev/null +++ b/src/modules/interface/menus/components/MenuItemListItem.vue @@ -0,0 +1,127 @@ + + diff --git a/src/modules/interface/menus/components/MenuList.vue b/src/modules/interface/menus/components/MenuList.vue new file mode 100644 index 000000000..e6bb1a521 --- /dev/null +++ b/src/modules/interface/menus/components/MenuList.vue @@ -0,0 +1,184 @@ + + diff --git a/src/modules/interface/menus/module.ts b/src/modules/interface/menus/module.ts index 114d63372..f1848faff 100644 --- a/src/modules/interface/menus/module.ts +++ b/src/modules/interface/menus/module.ts @@ -1,5 +1,5 @@ import { BasicLayout, definePlugin } from "@halo-dev/admin-shared"; -import MenuList from "./MenuList.vue"; +import Menus from "./Menus.vue"; import { IconListSettings } from "@halo-dev/components"; export default definePlugin({ @@ -13,7 +13,7 @@ export default definePlugin({ { path: "", name: "Menus", - component: MenuList, + component: Menus, }, ], }, diff --git a/src/modules/interface/menus/utils/__tests__/__snapshots__/index.spec.ts.snap b/src/modules/interface/menus/utils/__tests__/__snapshots__/index.spec.ts.snap new file mode 100644 index 000000000..170c11a4f --- /dev/null +++ b/src/modules/interface/menus/utils/__tests__/__snapshots__/index.spec.ts.snap @@ -0,0 +1,378 @@ +// Vitest Snapshot v1 + +exports[`buildMenuItemsTree > should match snapshot 1`] = ` +[ + { + "apiVersion": "v1alpha1", + "kind": "MenuItem", + "metadata": { + "creationTimestamp": "2022-08-05T04:22:03.377364Z", + "name": "411a3639-bf0d-4266-9cfb-14184259dab5", + "version": 1, + }, + "spec": { + "children": [], + "displayName": "首页", + "href": "https://ryanc.cc/", + "priority": 0, + }, + }, + { + "apiVersion": "v1alpha1", + "kind": "MenuItem", + "metadata": { + "creationTimestamp": "2022-08-05T04:19:37.252228Z", + "name": "40e4ba86-5c7e-43c2-b4c0-cee376d26564", + "version": 12, + }, + "spec": { + "children": [ + { + "apiVersion": "v1alpha1", + "kind": "MenuItem", + "metadata": { + "creationTimestamp": "2022-07-28T06:50:32.777556Z", + "name": "caeef383-3828-4039-9114-6f9ad3b4a37e", + "version": 4, + }, + "spec": { + "children": [], + "displayName": "Halo", + "href": "https://ryanc.cc/categories/halo", + "priority": 0, + }, + }, + { + "apiVersion": "v1alpha1", + "kind": "MenuItem", + "metadata": { + "creationTimestamp": "2022-08-05T04:22:03.377364Z", + "name": "ded1943d-9fdb-4563-83ee-2f04364872e0", + "version": 1, + }, + "spec": { + "children": [ + { + "apiVersion": "v1alpha1", + "kind": "MenuItem", + "metadata": { + "creationTimestamp": "2022-08-05T04:22:03.377364Z", + "name": "96b636bb-3e4a-44d1-8ea7-f9da9e876f45", + "version": 1, + }, + "spec": { + "children": [], + "displayName": "Spring Boot", + "href": "https://ryanc.cc/categories/spring-boot", + "priority": 0, + }, + }, + ], + "displayName": "Java", + "href": "https://ryanc.cc/categories/java", + "priority": 1, + }, + }, + ], + "displayName": "文章分类", + "href": "https://ryanc.cc/categories", + "priority": 1, + }, + }, +] +`; + +exports[`convertMenuTreeItemToMenuItem > should match snapshot 1`] = ` +{ + "apiVersion": "v1alpha1", + "kind": "MenuItem", + "metadata": { + "creationTimestamp": "2022-08-05T04:19:37.252228Z", + "name": "40e4ba86-5c7e-43c2-b4c0-cee376d26564", + "version": 12, + }, + "spec": { + "children": Set { + "caeef383-3828-4039-9114-6f9ad3b4a37e", + "ded1943d-9fdb-4563-83ee-2f04364872e0", + }, + "displayName": "文章分类", + "href": "https://ryanc.cc/categories", + "priority": 1, + }, +} +`; + +exports[`convertMenuTreeItemToMenuItem > should match snapshot 2`] = ` +{ + "apiVersion": "v1alpha1", + "kind": "MenuItem", + "metadata": { + "creationTimestamp": "2022-08-05T04:22:03.377364Z", + "name": "ded1943d-9fdb-4563-83ee-2f04364872e0", + "version": 1, + }, + "spec": { + "children": Set { + "96b636bb-3e4a-44d1-8ea7-f9da9e876f45", + }, + "displayName": "Java", + "href": "https://ryanc.cc/categories/java", + "priority": 1, + }, +} +`; + +exports[`convertTreeToMenuItems > will match snapshot 1`] = ` +[ + { + "apiVersion": "v1alpha1", + "kind": "MenuItem", + "metadata": { + "creationTimestamp": "2022-08-05T04:22:03.377364Z", + "name": "411a3639-bf0d-4266-9cfb-14184259dab5", + "version": 1, + }, + "spec": { + "children": [], + "displayName": "首页", + "href": "https://ryanc.cc/", + "priority": 0, + }, + }, + { + "apiVersion": "v1alpha1", + "kind": "MenuItem", + "metadata": { + "creationTimestamp": "2022-08-05T04:19:37.252228Z", + "name": "40e4ba86-5c7e-43c2-b4c0-cee376d26564", + "version": 12, + }, + "spec": { + "children": [ + "caeef383-3828-4039-9114-6f9ad3b4a37e", + "ded1943d-9fdb-4563-83ee-2f04364872e0", + ], + "displayName": "文章分类", + "href": "https://ryanc.cc/categories", + "priority": 1, + }, + }, + { + "apiVersion": "v1alpha1", + "kind": "MenuItem", + "metadata": { + "creationTimestamp": "2022-07-28T06:50:32.777556Z", + "name": "caeef383-3828-4039-9114-6f9ad3b4a37e", + "version": 4, + }, + "spec": { + "children": [], + "displayName": "Halo", + "href": "https://ryanc.cc/categories/halo", + "priority": 0, + }, + }, + { + "apiVersion": "v1alpha1", + "kind": "MenuItem", + "metadata": { + "creationTimestamp": "2022-08-05T04:22:03.377364Z", + "name": "ded1943d-9fdb-4563-83ee-2f04364872e0", + "version": 1, + }, + "spec": { + "children": [ + "96b636bb-3e4a-44d1-8ea7-f9da9e876f45", + ], + "displayName": "Java", + "href": "https://ryanc.cc/categories/java", + "priority": 1, + }, + }, + { + "apiVersion": "v1alpha1", + "kind": "MenuItem", + "metadata": { + "creationTimestamp": "2022-08-05T04:22:03.377364Z", + "name": "96b636bb-3e4a-44d1-8ea7-f9da9e876f45", + "version": 1, + }, + "spec": { + "children": [], + "displayName": "Spring Boot", + "href": "https://ryanc.cc/categories/spring-boot", + "priority": 0, + }, + }, +] +`; + +exports[`resetMenuItemsTreePriority > should match snapshot 1`] = ` +[ + { + "apiVersion": "v1alpha1", + "kind": "MenuItem", + "metadata": { + "creationTimestamp": "2022-08-05T04:22:03.377364Z", + "name": "411a3639-bf0d-4266-9cfb-14184259dab5", + "version": 1, + }, + "spec": { + "children": [], + "displayName": "首页", + "href": "https://ryanc.cc/", + "priority": 0, + }, + }, + { + "apiVersion": "v1alpha1", + "kind": "MenuItem", + "metadata": { + "creationTimestamp": "2022-08-05T04:19:37.252228Z", + "name": "40e4ba86-5c7e-43c2-b4c0-cee376d26564", + "version": 12, + }, + "spec": { + "children": [ + { + "apiVersion": "v1alpha1", + "kind": "MenuItem", + "metadata": { + "creationTimestamp": "2022-07-28T06:50:32.777556Z", + "name": "caeef383-3828-4039-9114-6f9ad3b4a37e", + "version": 4, + }, + "spec": { + "children": [], + "displayName": "Halo", + "href": "https://ryanc.cc/categories/halo", + "priority": 0, + }, + }, + { + "apiVersion": "v1alpha1", + "kind": "MenuItem", + "metadata": { + "creationTimestamp": "2022-08-05T04:22:03.377364Z", + "name": "ded1943d-9fdb-4563-83ee-2f04364872e0", + "version": 1, + }, + "spec": { + "children": [ + { + "apiVersion": "v1alpha1", + "kind": "MenuItem", + "metadata": { + "creationTimestamp": "2022-08-05T04:22:03.377364Z", + "name": "96b636bb-3e4a-44d1-8ea7-f9da9e876f45", + "version": 1, + }, + "spec": { + "children": [], + "displayName": "Spring Boot", + "href": "https://ryanc.cc/categories/spring-boot", + "priority": 0, + }, + }, + ], + "displayName": "Java", + "href": "https://ryanc.cc/categories/java", + "priority": 1, + }, + }, + ], + "displayName": "文章分类", + "href": "https://ryanc.cc/categories", + "priority": 1, + }, + }, +] +`; + +exports[`sortMenuItemsTree > will match snapshot 1`] = ` +[ + { + "apiVersion": "v1alpha1", + "kind": "MenuItem", + "metadata": { + "creationTimestamp": "2022-08-05T04:19:37.252228Z", + "name": "40e4ba86-5c7e-43c2-b4c0-cee376d26564", + "version": 12, + }, + "spec": { + "categoryRef": { + "name": "", + }, + "children": [ + { + "apiVersion": "v1alpha1", + "kind": "MenuItem", + "metadata": { + "creationTimestamp": "2022-08-05T04:22:03.377364Z", + "name": "ded1943d-9fdb-4563-83ee-2f04364872e0", + "version": 0, + }, + "spec": { + "categoryRef": { + "name": "", + }, + "children": [], + "displayName": "Java", + "href": "https://ryanc.cc/categories/java", + "pageRef": { + "name": "", + }, + "postRef": { + "name": "", + }, + "priority": 0, + "tagRef": { + "name": "", + }, + }, + }, + { + "apiVersion": "v1alpha1", + "kind": "MenuItem", + "metadata": { + "creationTimestamp": "2022-07-28T06:50:32.777556Z", + "name": "caeef383-3828-4039-9114-6f9ad3b4a37e", + "version": 4, + }, + "spec": { + "categoryRef": { + "name": "", + }, + "children": [], + "displayName": "Halo", + "href": "https://ryanc.cc/categories/halo", + "pageRef": { + "name": "", + }, + "postRef": { + "name": "", + }, + "priority": 1, + "tagRef": { + "name": "", + }, + }, + }, + ], + "displayName": "文章分类", + "href": "https://ryanc.cc/categories", + "pageRef": { + "name": "", + }, + "postRef": { + "name": "", + }, + "priority": 0, + "tagRef": { + "name": "", + }, + }, + }, +] +`; diff --git a/src/modules/interface/menus/utils/__tests__/index.spec.ts b/src/modules/interface/menus/utils/__tests__/index.spec.ts new file mode 100644 index 000000000..4b21ecbf4 --- /dev/null +++ b/src/modules/interface/menus/utils/__tests__/index.spec.ts @@ -0,0 +1,269 @@ +import { describe, expect, it } from "vitest"; +import type { MenuTreeItem } from "../index"; +import { + buildMenuItemsTree, + convertMenuTreeItemToMenuItem, + convertTreeToMenuItems, + getChildrenNames, + resetMenuItemsTreePriority, + sortMenuItemsTree, +} from "../index"; +import type { MenuItem } from "@halo-dev/api-client"; + +const rawMenuItems: MenuItem[] = [ + { + spec: { + displayName: "文章分类", + href: "https://ryanc.cc/categories", + // @ts-ignore + children: [ + "caeef383-3828-4039-9114-6f9ad3b4a37e", + "ded1943d-9fdb-4563-83ee-2f04364872e0", + ], + priority: 1, + }, + apiVersion: "v1alpha1", + kind: "MenuItem", + metadata: { + name: "40e4ba86-5c7e-43c2-b4c0-cee376d26564", + version: 12, + creationTimestamp: "2022-08-05T04:19:37.252228Z", + }, + }, + { + spec: { + displayName: "Halo", + href: "https://ryanc.cc/categories/halo", + // @ts-ignore + children: [], + priority: 0, + }, + apiVersion: "v1alpha1", + kind: "MenuItem", + metadata: { + name: "caeef383-3828-4039-9114-6f9ad3b4a37e", + version: 4, + creationTimestamp: "2022-07-28T06:50:32.777556Z", + }, + }, + { + spec: { + displayName: "Java", + href: "https://ryanc.cc/categories/java", + // @ts-ignore + children: ["96b636bb-3e4a-44d1-8ea7-f9da9e876f45"], + priority: 1, + }, + apiVersion: "v1alpha1", + kind: "MenuItem", + metadata: { + name: "ded1943d-9fdb-4563-83ee-2f04364872e0", + version: 1, + creationTimestamp: "2022-08-05T04:22:03.377364Z", + }, + }, + { + spec: { + displayName: "Spring Boot", + href: "https://ryanc.cc/categories/spring-boot", + // @ts-ignore + children: [], + priority: 0, + }, + apiVersion: "v1alpha1", + kind: "MenuItem", + metadata: { + name: "96b636bb-3e4a-44d1-8ea7-f9da9e876f45", + version: 1, + creationTimestamp: "2022-08-05T04:22:03.377364Z", + }, + }, + { + spec: { + displayName: "首页", + href: "https://ryanc.cc/", + // @ts-ignore + children: [], + priority: 0, + }, + apiVersion: "v1alpha1", + kind: "MenuItem", + metadata: { + name: "411a3639-bf0d-4266-9cfb-14184259dab5", + version: 1, + creationTimestamp: "2022-08-05T04:22:03.377364Z", + }, + }, +]; + +describe("buildMenuItemsTree", () => { + it("should match snapshot", () => { + expect(buildMenuItemsTree(rawMenuItems)).toMatchSnapshot(); + }); + + it("should be sorted correctly", () => { + const menuItems = buildMenuItemsTree(rawMenuItems); + expect(menuItems[0].spec.priority).toBe(0); + expect(menuItems[1].spec.priority).toBe(1); + + // children should be sorted + expect(menuItems[1].spec.children[0].spec.priority).toBe(0); + expect(menuItems[1].spec.children[1].spec.priority).toBe(1); + expect(menuItems[1].spec.children[1].spec.children[0].spec.priority).toBe( + 0 + ); + + expect(menuItems[0].spec.displayName).toBe("首页"); + expect(menuItems[1].spec.displayName).toBe("文章分类"); + expect(menuItems[1].spec.children[0].spec.displayName).toBe("Halo"); + expect(menuItems[1].spec.children[1].spec.displayName).toBe("Java"); + expect( + menuItems[1].spec.children[1].spec.children[0].spec.displayName + ).toBe("Spring Boot"); + }); +}); + +describe("convertTreeToMenuItems", () => { + it("will match snapshot", function () { + const menuTreeItems = buildMenuItemsTree(rawMenuItems); + expect(convertTreeToMenuItems(menuTreeItems)).toMatchSnapshot(); + }); +}); + +describe("sortMenuItemsTree", () => { + it("will match snapshot", () => { + const tree: MenuTreeItem[] = [ + { + apiVersion: "v1alpha1", + kind: "MenuItem", + metadata: { + creationTimestamp: "2022-08-05T04:19:37.252228Z", + name: "40e4ba86-5c7e-43c2-b4c0-cee376d26564", + version: 12, + }, + spec: { + categoryRef: { + name: "", + }, + children: [ + { + apiVersion: "v1alpha1", + kind: "MenuItem", + metadata: { + creationTimestamp: "2022-07-28T06:50:32.777556Z", + name: "caeef383-3828-4039-9114-6f9ad3b4a37e", + version: 4, + }, + spec: { + categoryRef: { + name: "", + }, + children: [], + priority: 1, + displayName: "Halo", + href: "https://ryanc.cc/categories/halo", + pageRef: { + name: "", + }, + postRef: { + name: "", + }, + tagRef: { + name: "", + }, + }, + }, + { + apiVersion: "v1alpha1", + kind: "MenuItem", + metadata: { + creationTimestamp: "2022-08-05T04:22:03.377364Z", + name: "ded1943d-9fdb-4563-83ee-2f04364872e0", + version: 0, + }, + spec: { + categoryRef: { + name: "", + }, + children: [], + priority: 0, + displayName: "Java", + href: "https://ryanc.cc/categories/java", + pageRef: { + name: "", + }, + postRef: { + name: "", + }, + tagRef: { + name: "", + }, + }, + }, + ], + priority: 0, + displayName: "文章分类", + href: "https://ryanc.cc/categories", + pageRef: { + name: "", + }, + postRef: { + name: "", + }, + tagRef: { + name: "", + }, + }, + }, + ]; + + expect(sortMenuItemsTree(tree)).toMatchSnapshot(); + }); +}); + +describe("resetMenuItemsTreePriority", () => { + it("should match snapshot", function () { + expect( + resetMenuItemsTreePriority(buildMenuItemsTree(rawMenuItems)) + ).toMatchSnapshot(); + }); +}); + +describe("getChildrenNames", () => { + it("should return correct names", () => { + const menuTreeItems = buildMenuItemsTree(rawMenuItems); + expect(getChildrenNames(menuTreeItems[0])).toEqual([]); + expect(getChildrenNames(menuTreeItems[1])).toEqual([ + "caeef383-3828-4039-9114-6f9ad3b4a37e", + "ded1943d-9fdb-4563-83ee-2f04364872e0", + "96b636bb-3e4a-44d1-8ea7-f9da9e876f45", + ]); + + expect(getChildrenNames(menuTreeItems[1].spec.children[0])).toEqual([]); + }); +}); + +describe("convertMenuTreeItemToMenuItem", () => { + it("should match snapshot", () => { + const menuTreeItems = buildMenuItemsTree(rawMenuItems); + expect(convertMenuTreeItemToMenuItem(menuTreeItems[1])).toMatchSnapshot(); + expect( + convertMenuTreeItemToMenuItem(menuTreeItems[1].spec.children[1]) + ).toMatchSnapshot(); + }); + + it("should return correct MenuItem", () => { + const menuTreeItems = buildMenuItemsTree(rawMenuItems); + expect( + convertMenuTreeItemToMenuItem(menuTreeItems[1]).spec.displayName + ).toBe("文章分类"); + expect( + convertMenuTreeItemToMenuItem(menuTreeItems[1]).spec.children + ).toStrictEqual( + new Set([ + "caeef383-3828-4039-9114-6f9ad3b4a37e", + "ded1943d-9fdb-4563-83ee-2f04364872e0", + ]) + ); + }); +}); diff --git a/src/modules/interface/menus/utils/index.ts b/src/modules/interface/menus/utils/index.ts new file mode 100644 index 000000000..da2ae5869 --- /dev/null +++ b/src/modules/interface/menus/utils/index.ts @@ -0,0 +1,267 @@ +import type { MenuItem, MenuItemSpec } from "@halo-dev/api-client"; +import cloneDeep from "lodash.clonedeep"; + +export interface MenuTreeItemSpec extends Omit { + children: MenuTreeItem[]; +} + +export interface MenuTreeItem extends Omit { + spec: MenuTreeItemSpec; +} + +/** + * Convert a flat array of menu items into flattens a menu tree. + * + * Example: + * + * ```json + * [ + * { + * "spec": { + * "displayName": "文章分类", + * "href": "https://ryanc.cc/categories", + * "children": [ + * "caeef383-3828-4039-9114-6f9ad3b4a37e", + * "ded1943d-9fdb-4563-83ee-2f04364872e0" + * ] + * }, + * "apiVersion": "v1alpha1", + * "kind": "MenuItem", + * "metadata": { + * "name": "40e4ba86-5c7e-43c2-b4c0-cee376d26564", + * "version": 12, + * "creationTimestamp": "2022-08-05T04:19:37.252228Z" + * } + * }, + * { + * "spec": { + * "displayName": "Halo", + * "href": "https://ryanc.cc/categories/halo", + * "children": [] + * }, + * "apiVersion": "v1alpha1", + * "kind": "MenuItem", + * "metadata": { + * "name": "caeef383-3828-4039-9114-6f9ad3b4a37e", + * "version": 4, + * "creationTimestamp": "2022-07-28T06:50:32.777556Z" + * } + * }, + * { + * "spec": { + * "displayName": "Java", + * "href": "https://ryanc.cc/categories/java", + * "children": [] + * }, + * "apiVersion": "v1alpha1", + * "kind": "MenuItem", + * "metadata": { + * "name": "ded1943d-9fdb-4563-83ee-2f04364872e0", + * "version": 1, + * "creationTimestamp": "2022-08-05T04:22:03.377364Z" + * } + * } + * ] + * ``` + * + * will be transformed to: + * + * ```json + * [ + * { + * "spec": { + * "displayName": "文章分类", + * "href": "https://ryanc.cc/categories", + * "children": [ + * { + * "spec": { + * "displayName": "Halo", + * "href": "https://ryanc.cc/categories/halo", + * "children": [] + * }, + * "apiVersion": "v1alpha1", + * "kind": "MenuItem", + * "metadata": { + * "name": "caeef383-3828-4039-9114-6f9ad3b4a37e", + * "version": 4, + * "creationTimestamp": "2022-07-28T06:50:32.777556Z" + * } + * }, + * { + * "spec": { + * "displayName": "Java", + * "href": "https://ryanc.cc/categories/java", + * "children": [] + * }, + * "apiVersion": "v1alpha1", + * "kind": "MenuItem", + * "metadata": { + * "name": "ded1943d-9fdb-4563-83ee-2f04364872e0", + * "version": 1, + * "creationTimestamp": "2022-08-05T04:22:03.377364Z" + * } + * } + * ] + * }, + * "apiVersion": "v1alpha1", + * "kind": "MenuItem", + * "metadata": { + * "name": "40e4ba86-5c7e-43c2-b4c0-cee376d26564", + * "version": 12, + * "creationTimestamp": "2022-08-05T04:19:37.252228Z" + * } + * } + * ] + * ``` + * + * @param menuItems + */ +export function buildMenuItemsTree(menuItems: MenuItem[]): MenuTreeItem[] { + const menuItemsToUpdate = cloneDeep(menuItems); + + const menuItemsMap = {}; + const parentMap = {}; + + menuItemsToUpdate.forEach((menuItem) => { + menuItemsMap[menuItem.metadata.name] = menuItem; + // @ts-ignore + menuItem.spec.children.forEach((child) => { + parentMap[child] = menuItem.metadata.name; + }); + // @ts-ignore + menuItem.spec.children = []; + }); + + menuItemsToUpdate.forEach((menuItem) => { + const parentName = parentMap[menuItem.metadata.name]; + if (parentName && menuItemsMap[parentName]) { + menuItemsMap[parentName].spec.children.push(menuItem); + } + }); + + const menuTreeItems = menuItemsToUpdate.filter( + (node) => parentMap[node.metadata.name] === undefined + ); + + return sortMenuItemsTree(menuTreeItems); +} + +/** + * Sort a menu tree by priority. + * + * @param menuTreeItems + */ +export function sortMenuItemsTree( + menuTreeItems: MenuTreeItem[] | MenuItem[] +): MenuTreeItem[] { + return menuTreeItems + .sort((a, b) => { + if (a.spec.priority < b.spec.priority) { + return -1; + } + if (a.spec.priority > b.spec.priority) { + return 1; + } + return 0; + }) + .map((menuTreeItem) => { + if (menuTreeItem.spec.children.length) { + return { + ...menuTreeItem, + spec: { + ...menuTreeItem.spec, + children: sortMenuItemsTree(menuTreeItem.spec.children), + }, + }; + } + return menuTreeItem; + }); +} + +/** + * Reset the menu tree item's priority. + * + * @param menuItems + */ +export function resetMenuItemsTreePriority( + menuItems: MenuTreeItem[] +): MenuTreeItem[] { + for (let i = 0; i < menuItems.length; i++) { + menuItems[i].spec.priority = i; + if (menuItems[i].spec.children) { + resetMenuItemsTreePriority(menuItems[i].spec.children); + } + } + return menuItems; +} + +/** + * Convert a menu tree items into a flat array of menu. + * + * @param menuTreeItems + */ +export function convertTreeToMenuItems(menuTreeItems: MenuTreeItem[]) { + const menuItems: MenuItem[] = []; + const menuItemsMap = new Map(); + const convertMenuItem = (node: MenuTreeItem | undefined) => { + if (!node) { + return; + } + const children = node.spec.children || []; + menuItemsMap.set(node.metadata.name, { + ...node, + spec: { + ...node.spec, + // @ts-ignore + children: children.map((child) => child.metadata.name), + }, + }); + children.forEach((child) => { + convertMenuItem(child); + }); + }; + menuTreeItems.forEach((node) => { + convertMenuItem(node); + }); + menuItemsMap.forEach((node) => { + menuItems.push(node); + }); + return menuItems; +} + +export function getChildrenNames(menuTreeItem: MenuTreeItem): string[] { + const childrenNames: string[] = []; + + function getChildrenNamesRecursive(menuTreeItem: MenuTreeItem) { + if (menuTreeItem.spec.children) { + menuTreeItem.spec.children.forEach((child) => { + childrenNames.push(child.metadata.name); + getChildrenNamesRecursive(child); + }); + } + } + + getChildrenNamesRecursive(menuTreeItem); + + return childrenNames; +} + +/** + * Convert {@link MenuTreeItem} to {@link MenuItem} with flat children name array. + * + * @param menuTreeItem + */ +export function convertMenuTreeItemToMenuItem( + menuTreeItem: MenuTreeItem +): MenuItem { + const childNames = menuTreeItem.spec.children.map( + (child) => child.metadata.name + ); + return { + ...menuTreeItem, + spec: { + ...menuTreeItem.spec, + children: new Set(childNames), + }, + }; +}