From 5e8e828ae6c6391f83929bd774bd898f96487f77 Mon Sep 17 00:00:00 2001 From: Ryan Wang Date: Sat, 19 Oct 2024 23:55:02 +0800 Subject: [PATCH] feat: add supports for clone post --- ui/package.json | 3 + ui/pnpm-lock.yaml | 76 +++++++++++++++++++++ ui/src/class/postCloner.ts | 65 ++++++++++++++++++ ui/src/class/postOperations.ts | 14 ++-- ui/src/components/PostCloneDropdownItem.vue | 24 +++++++ ui/src/index.ts | 16 +++-- ui/src/utils/id.ts | 7 ++ 7 files changed, 196 insertions(+), 9 deletions(-) create mode 100644 ui/src/class/postCloner.ts create mode 100644 ui/src/components/PostCloneDropdownItem.vue create mode 100644 ui/src/utils/id.ts diff --git a/ui/package.json b/ui/package.json index d81665f..5736718 100644 --- a/ui/package.json +++ b/ui/package.json @@ -13,11 +13,13 @@ "@halo-dev/api-client": "^2.20.0", "@halo-dev/components": "^2.20.0", "@halo-dev/console-shared": "^2.20.0", + "@tanstack/vue-query": "4", "@vueuse/core": "^11.1.0", "@vueuse/router": "^11.1.0", "axios": "^1.7.7", "gray-matter": "^4.0.3", "js-yaml": "^4.1.0", + "lodash-es": "^4.17.21", "markdown-it": "^14.1.0", "markdown-it-anchor": "^9.2.0", "turndown": "^7.2.0", @@ -30,6 +32,7 @@ "@tsconfig/node20": "^20.1.4", "@types/js-yaml": "^4.0.9", "@types/jsdom": "^21.1.7", + "@types/lodash-es": "^4.17.12", "@types/markdown-it": "^14.1.2", "@types/node": "^20.16.12", "@types/turndown": "^5.0.5", diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index ba950be..e13421f 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: '@halo-dev/console-shared': specifier: ^2.20.0 version: 2.20.0(axios@1.7.7)(vue-router@4.2.5(vue@3.5.12(typescript@5.5.4)))(vue@3.5.12(typescript@5.5.4)) + '@tanstack/vue-query': + specifier: '4' + version: 4.37.1(vue@3.5.12(typescript@5.5.4)) '@vueuse/core': specifier: ^11.1.0 version: 11.1.0(vue@3.5.12(typescript@5.5.4)) @@ -32,6 +35,9 @@ importers: js-yaml: specifier: ^4.1.0 version: 4.1.0 + lodash-es: + specifier: ^4.17.21 + version: 4.17.21 markdown-it: specifier: ^14.1.0 version: 14.1.0 @@ -63,6 +69,9 @@ importers: '@types/jsdom': specifier: ^21.1.7 version: 21.1.7 + '@types/lodash-es': + specifier: ^4.17.12 + version: 4.17.12 '@types/markdown-it': specifier: ^14.1.2 version: 14.1.2 @@ -772,6 +781,22 @@ packages: '@sinclair/typebox@0.27.8': resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + '@tanstack/match-sorter-utils@8.19.4': + resolution: {integrity: sha512-Wo1iKt2b9OT7d+YGhvEPD3DXvPv2etTusIMhMUoG7fbhmxcXCtIjJDEygy91Y2JFlwGyjqiBPRozme7UD8hoqg==} + engines: {node: '>=12'} + + '@tanstack/query-core@4.36.1': + resolution: {integrity: sha512-DJSilV5+ytBP1FbFcEJovv4rnnm/CokuVvrBEtW/Va9DvuJ3HksbXUJEpI0aV1KtuL4ZoO9AVE6PyNLzF7tLeA==} + + '@tanstack/vue-query@4.37.1': + resolution: {integrity: sha512-QzCQ94g2oZQcEfI4nfqa6Qr3aFXtXiEH17Jho+QFl73c7epqsWNcyP3ovF1fgJz5jEOE5OYtwgkoaRKIRaSigg==} + peerDependencies: + '@vue/composition-api': ^1.1.2 + vue: ^2.5.0 || ^3.0.0 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + '@tsconfig/node20@20.1.4': resolution: {integrity: sha512-sqgsT69YFeLWf5NtJ4Xq/xAF8p4ZQHlmGW74Nu2tD4+g5fAsposc4ZfaaPixVu4y01BEiDCWLRDCvDM5JOsRxg==} @@ -790,6 +815,12 @@ packages: '@types/linkify-it@5.0.0': resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} + '@types/lodash-es@4.17.12': + resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} + + '@types/lodash@4.17.12': + resolution: {integrity: sha512-sviUmCE8AYdaF/KIHLDJBQgeYzPBI0vf/17NaYehBJfYD1j6/L95Slh07NlyK2iNyBNaEkb3En2jRt+a8y3xZQ==} + '@types/markdown-it@14.1.2': resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} @@ -1935,6 +1966,9 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash-es@4.17.21: + resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} @@ -2340,6 +2374,9 @@ packages: resolution: {integrity: sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==} engines: {node: '>= 14.16.0'} + remove-accents@0.5.0: + resolution: {integrity: sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==} + requires-port@1.0.0: resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} @@ -2766,6 +2803,17 @@ packages: vue-component-type-helpers@2.0.24: resolution: {integrity: sha512-Jr5N8QVYEcbQuMN1LRgvg61758G8HTnzUlQsAFOxx6Y6X8kmhJ7C+jOvWsQruYxi3uHhhS6BghyRlyiwO99DBg==} + vue-demi@0.13.11: + resolution: {integrity: sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==} + engines: {node: '>=12'} + hasBin: true + peerDependencies: + '@vue/composition-api': ^1.0.0-rc.1 + vue: ^3.0.0-0 || ^2.6.0 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + vue-demi@0.14.10: resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} engines: {node: '>=12'} @@ -3358,6 +3406,20 @@ snapshots: '@sinclair/typebox@0.27.8': {} + '@tanstack/match-sorter-utils@8.19.4': + dependencies: + remove-accents: 0.5.0 + + '@tanstack/query-core@4.36.1': {} + + '@tanstack/vue-query@4.37.1(vue@3.5.12(typescript@5.5.4))': + dependencies: + '@tanstack/match-sorter-utils': 8.19.4 + '@tanstack/query-core': 4.36.1 + '@vue/devtools-api': 6.5.0 + vue: 3.5.12(typescript@5.5.4) + vue-demi: 0.13.11(vue@3.5.12(typescript@5.5.4)) + '@tsconfig/node20@20.1.4': {} '@types/estree@1.0.5': {} @@ -3374,6 +3436,12 @@ snapshots: '@types/linkify-it@5.0.0': {} + '@types/lodash-es@4.17.12': + dependencies: + '@types/lodash': 4.17.12 + + '@types/lodash@4.17.12': {} + '@types/markdown-it@14.1.2': dependencies: '@types/linkify-it': 5.0.0 @@ -4796,6 +4864,8 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash-es@4.17.21: {} + lodash.merge@4.6.2: {} lodash@4.17.21: {} @@ -5208,6 +5278,8 @@ snapshots: readdirp@4.0.2: {} + remove-accents@0.5.0: {} + requires-port@1.0.0: {} resolve-from@4.0.0: {} @@ -5642,6 +5714,10 @@ snapshots: vue-component-type-helpers@2.0.24: {} + vue-demi@0.13.11(vue@3.5.12(typescript@5.5.4)): + dependencies: + vue: 3.5.12(typescript@5.5.4) + vue-demi@0.14.10(vue@3.5.12(typescript@5.5.4)): dependencies: vue: 3.5.12(typescript@5.5.4) diff --git a/ui/src/class/postCloner.ts b/ui/src/class/postCloner.ts new file mode 100644 index 0000000..357064e --- /dev/null +++ b/ui/src/class/postCloner.ts @@ -0,0 +1,65 @@ +import { randomUUID } from "@/utils/id"; +import { + consoleApiClient, + type ContentWrapper, + type Post, + type PostRequest, +} from "@halo-dev/api-client"; +import { Toast } from "@halo-dev/components"; +import { cloneDeep, set } from "lodash-es"; + +class PostCloner { + static async clonePost(post: Post): Promise { + try { + const originalContent = await this.fetchPostContent(post.metadata.name); + + const newPostData = this.prepareNewPostData(post, originalContent); + + await consoleApiClient.content.post.draftPost({ + postRequest: newPostData, + }); + + Toast.success("文章克隆成功,如果列表没有刷新,请手动刷新一次"); + } catch (error) { + console.error("Failed to clone post", error); + Toast.error("克隆文章失败"); + } + } + + private static async fetchPostContent( + postName: string, + ): Promise { + const { data } = await consoleApiClient.content.post.fetchPostHeadContent({ + name: postName, + }); + + return data; + } + + private static prepareNewPostData( + originalPost: Post, + content: ContentWrapper, + ): PostRequest { + const postToCreate = cloneDeep(originalPost); + set(postToCreate, "spec.baseSnapshot", ""); + set(postToCreate, "spec.headSnapshot", ""); + set(postToCreate, "spec.releaseSnapshot", ""); + set( + postToCreate, + "spec.slug", + `${originalPost.spec.slug}-${randomUUID().split("-")[0]}`, + ); + set(postToCreate, "spec.title", originalPost.spec.title + "(副本)"); + set(postToCreate, "spec.publish", false); + set(postToCreate, "metadata", { + name: randomUUID(), + }); + + return { + post: postToCreate, + content: content, + }; + } +} + +export default PostCloner; diff --git a/ui/src/class/postOperations.ts b/ui/src/class/postOperations.ts index d24edc1..5e72622 100644 --- a/ui/src/class/postOperations.ts +++ b/ui/src/class/postOperations.ts @@ -29,10 +29,15 @@ export class PostOperations { toType, ); - const convertedContent = converter.convert(post, content); + const convertedRawContent = converter.convert(post, content); try { - await this.updatePostContent(post, toType, convertedContent); + await this.updatePostContent( + post, + toType, + convertedRawContent, + content.content || "", + ); Toast.success("转换完成"); } catch (error) { if (error instanceof AxiosError) { @@ -44,6 +49,7 @@ export class PostOperations { private static async updatePostContent( post: Post, rawType: string, + raw: string, content: string, ): Promise { const published = post.spec.publish; @@ -52,8 +58,8 @@ export class PostOperations { name: post.metadata.name, content: { rawType, - raw: content, - content, + raw: raw, + content: content, }, }); diff --git a/ui/src/components/PostCloneDropdownItem.vue b/ui/src/components/PostCloneDropdownItem.vue new file mode 100644 index 0000000..f6cde81 --- /dev/null +++ b/ui/src/components/PostCloneDropdownItem.vue @@ -0,0 +1,24 @@ + + diff --git a/ui/src/index.ts b/ui/src/index.ts index 01b5215..a1df7f4 100644 --- a/ui/src/index.ts +++ b/ui/src/index.ts @@ -5,6 +5,7 @@ import { markRaw } from "vue"; import { ContentExporter } from "./class/contentExporter"; import { PostOperations } from "./class/postOperations"; import ConverterEditor from "./components/ConverterEditor.vue"; +import PostCloneDropdownItem from "./components/PostCloneDropdownItem.vue"; export default definePlugin({ components: {}, @@ -36,20 +37,19 @@ export default definePlugin({ }, ]; }, - // @ts-expect-error - "post:list-item:operation:create": () => { + // @ts-expect-error don't important + // Needs upstream to fix this issue + "post:list-item:operation:create": (post: ListedPost) => { return [ { priority: 21, component: markRaw(VDropdownDivider), - permissions: ["system:posts:view"], }, { priority: 22, component: markRaw(VDropdownItem), label: "转换", visible: true, - permissions: ["system:posts:view"], children: [ { priority: 0, @@ -90,7 +90,6 @@ export default definePlugin({ component: markRaw(VDropdownItem), label: "导出", visible: true, - permissions: ["system:posts:view"], children: [ { priority: 0, @@ -112,6 +111,13 @@ export default definePlugin({ }, ], }, + { + priority: 24, + component: markRaw(PostCloneDropdownItem), + props: { + post: post, + }, + }, ]; }, }, diff --git a/ui/src/utils/id.ts b/ui/src/utils/id.ts new file mode 100644 index 0000000..8cfe5d7 --- /dev/null +++ b/ui/src/utils/id.ts @@ -0,0 +1,7 @@ +export function randomUUID() { + return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) { + const r = (Math.random() * 16) | 0, + v = c === "x" ? r : (r & 0x3) | 0x8; + return v.toString(16); + }); +}