From 70eb0394682edec67b69fca0ca291c48a1ceabe5 Mon Sep 17 00:00:00 2001 From: Ryan Wang Date: Fri, 25 Aug 2023 04:02:12 -0500 Subject: [PATCH] feat: add extension point for extend data list operation items (#4452) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### What type of PR is this? /area console /kind feature /milestone 2.9.x #### What this PR does / why we need it: 添加扩展数据列表中操作按钮列表的基础能力,并为文章和插件管理列表的操作按钮列表添加扩展点以测试此扩展能力。 todo: - [x] 场景测试 - [x] 文档 #### Which issue(s) this PR fixes: Ref https://github.com/halo-dev/halo/issues/4177 #### Special notes for your reviewer: 可以使用以下插件进行测试: - 源码:[plugin-export-md.zip](https://github.com/halo-dev/halo/files/12436956/plugin-export-md.zip) - 可安装的 JAR 包:[plugin-export-md-1.0.0-SNAPSHOT.jar.zip](https://github.com/halo-dev/halo/files/12436950/plugin-export-md-1.0.0-SNAPSHOT.jar.zip) 安装之后可以在文章列表的操作按钮列表中新增一个 `导出为 Markdown 文档` 的按钮,点击之后会导出一个 Markdown 文档。 image #### Does this PR introduce a user-facing change? ```release-note Console 端的文章和插件列表的操作按钮列表支持扩展。 ``` --- .../entity-listitem-operation.md | 68 +++++++++++ console/packages/shared/src/index.ts | 1 + console/packages/shared/src/states/entity.ts | 12 ++ console/packages/shared/src/types/plugin.ts | 10 ++ .../components/entity/EntityDropdownItems.vue | 66 ++++++++++ .../use-entity-extension-points.ts | 36 ++++++ .../posts/components/PostListItem.vue | 68 ++++++++--- .../plugins/components/PluginListItem.vue | 115 +++++++++++++----- 8 files changed, 327 insertions(+), 49 deletions(-) create mode 100644 console/docs/extension-points/entity-listitem-operation.md create mode 100644 console/packages/shared/src/states/entity.ts create mode 100644 console/src/components/entity/EntityDropdownItems.vue create mode 100644 console/src/composables/use-entity-extension-points.ts diff --git a/console/docs/extension-points/entity-listitem-operation.md b/console/docs/extension-points/entity-listitem-operation.md new file mode 100644 index 000000000..230af936a --- /dev/null +++ b/console/docs/extension-points/entity-listitem-operation.md @@ -0,0 +1,68 @@ +# Entity 数据列表操作菜单扩展点 + +## 原由 + +目前 Halo 2 的 Console 中,展示数据列表是统一使用 Entity 组件,Entity 组件中提供了用于放置操作按钮的插槽,此扩展点用于支持通过插件扩展部分数据列表的操作菜单项。 + +## 定义方式 + +目前支持扩展的数据列表: + +- 文章:`"post:list-item:operation:create"?: () => | EntityDropdownItem[] | Promise[]>` +- 插件:`"plugin:list-item:operation:create"?: () => | EntityDropdownItem[] | Promise[]>` + +示例: + +> 此示例是在文章列表中添加一个`导出为 Markdown 文档`的操作菜单项。 + +```ts +import type { ListedPost } from "@halo-dev/api-client"; +import { VDropdownItem } from "@halo-dev/components"; +import { definePlugin } from "@halo-dev/console-shared"; +import axios from "axios"; +import { markRaw } from "vue"; + +export default definePlugin({ + extensionPoints: { + "post:list-item:operation:create": () => { + return [ + { + priority: 21, + component: markRaw(VDropdownItem), + label: "导出为 Markdown 文档", + visible: true, + permissions: [], + action: async (post: ListedPost) => { + const { data } = await axios.get( + `/apis/api.console.halo.run/v1alpha1/posts/${post.post.metadata.name}/head-content` + ); + const blob = new Blob([data.raw], { + type: "text/plain;charset=utf-8", + }); + const url = window.URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = `${post.post.spec.title}.md`; + link.click(); + }, + }, + ]; + }, + }, +}); +``` + +`EntityDropdownItem` 类型: + +```ts +export interface EntityDropdownItem { + priority: number; // 优先级,越小越靠前 + component: Raw; // 菜单项组件,可以使用 `@halo-dev/components` 中提供的 `VDropdownItem`,也可以自定义 + props?: Record; // 组件的 props + action?: (item?: T) => void; // 点击事件 + label?: string; // 菜单项名称 + visible?: boolean; // 是否可见 + permissions?: string[]; // 权限 + children?: EntityDropdownItem[]; // 子菜单 +} +``` diff --git a/console/packages/shared/src/index.ts b/console/packages/shared/src/index.ts index 336f4ce04..57025125e 100644 --- a/console/packages/shared/src/index.ts +++ b/console/packages/shared/src/index.ts @@ -7,3 +7,4 @@ export * from "./states/editor"; export * from "./states/plugin-tab"; export * from "./states/comment-subject-ref"; export * from "./states/backup"; +export * from "./states/entity"; diff --git a/console/packages/shared/src/states/entity.ts b/console/packages/shared/src/states/entity.ts new file mode 100644 index 000000000..3f8bb2955 --- /dev/null +++ b/console/packages/shared/src/states/entity.ts @@ -0,0 +1,12 @@ +import type { Component, Raw } from "vue"; + +export interface EntityDropdownItem { + priority: number; + component: Raw; + props?: Record; + action?: (item?: T) => void; + label?: string; + visible?: boolean; + permissions?: string[]; + children?: EntityDropdownItem[]; +} diff --git a/console/packages/shared/src/types/plugin.ts b/console/packages/shared/src/types/plugin.ts index 68cd84ea7..5dcdfee49 100644 --- a/console/packages/shared/src/types/plugin.ts +++ b/console/packages/shared/src/types/plugin.ts @@ -6,6 +6,8 @@ import type { EditorProvider, PluginTab } from ".."; import type { AnyExtension } from "@tiptap/vue-3"; import type { CommentSubjectRefProvider } from "@/states/comment-subject-ref"; import type { BackupTab } from "@/states/backup"; +import type { EntityDropdownItem } from "@/states/entity"; +import type { ListedPost, Plugin } from "@halo-dev/api-client"; export interface RouteRecordAppend { parentName: RouteRecordName; @@ -31,6 +33,14 @@ export interface ExtensionPoint { "comment:subject-ref:create"?: () => CommentSubjectRefProvider[]; "backup:tabs:create"?: () => BackupTab[] | Promise; + + "post:list-item:operation:create"?: () => + | EntityDropdownItem[] + | Promise[]>; + + "plugin:list-item:operation:create"?: () => + | EntityDropdownItem[] + | Promise[]>; } export interface PluginModule { diff --git a/console/src/components/entity/EntityDropdownItems.vue b/console/src/components/entity/EntityDropdownItems.vue new file mode 100644 index 000000000..27738fcb2 --- /dev/null +++ b/console/src/components/entity/EntityDropdownItems.vue @@ -0,0 +1,66 @@ + + + diff --git a/console/src/composables/use-entity-extension-points.ts b/console/src/composables/use-entity-extension-points.ts new file mode 100644 index 000000000..97bff52f5 --- /dev/null +++ b/console/src/composables/use-entity-extension-points.ts @@ -0,0 +1,36 @@ +import { usePluginModuleStore } from "@/stores/plugin"; +import type { + EntityDropdownItem, + PluginModule, +} from "@halo-dev/console-shared"; +import { onMounted, ref } from "vue"; + +export function useEntityDropdownItemExtensionPoint( + extensionPointName: string, + presets: EntityDropdownItem[] +) { + const { pluginModules } = usePluginModuleStore(); + + const dropdownItems = ref[]>(presets); + + onMounted(() => { + pluginModules.forEach((pluginModule: PluginModule) => { + const { extensionPoints } = pluginModule; + if (!extensionPoints?.[extensionPointName]) { + return; + } + + const items = extensionPoints[ + extensionPointName + ]() as EntityDropdownItem[]; + + dropdownItems.value.push(...items); + }); + + dropdownItems.value.sort((a, b) => { + return a.priority - b.priority; + }); + }); + + return { dropdownItems }; +} diff --git a/console/src/modules/contents/posts/components/PostListItem.vue b/console/src/modules/contents/posts/components/PostListItem.vue index da75c781e..ab1fa2467 100644 --- a/console/src/modules/contents/posts/components/PostListItem.vue +++ b/console/src/modules/contents/posts/components/PostListItem.vue @@ -25,10 +25,15 @@ import { inject } from "vue"; import type { Ref } from "vue"; import { ref } from "vue"; import { computed } from "vue"; +import { markRaw } from "vue"; +import { useRouter } from "vue-router"; +import { useEntityDropdownItemExtensionPoint } from "@/composables/use-entity-extension-points"; +import EntityDropdownItems from "@/components/entity/EntityDropdownItems.vue"; const { currentUserHasPermission } = usePermission(); const { t } = useI18n(); const queryClient = useQueryClient(); +const router = useRouter(); const props = withDefaults( defineProps<{ @@ -113,6 +118,51 @@ const handleDelete = async () => { }, }); }; + +const { dropdownItems } = useEntityDropdownItemExtensionPoint( + "post:list-item:operation:create", + [ + { + priority: 10, + component: markRaw(VDropdownItem), + label: t("core.common.buttons.edit"), + visible: true, + permissions: [], + action: () => { + router.push({ + name: "PostEditor", + query: { name: props.post.post.metadata.name }, + }); + }, + }, + { + priority: 20, + component: markRaw(VDropdownItem), + label: t("core.common.buttons.setting"), + visible: true, + permissions: [], + action: () => { + emit("open-setting-modal", props.post.post); + }, + }, + { + priority: 30, + component: markRaw(VDropdownDivider), + visible: true, + }, + { + priority: 40, + component: markRaw(VDropdownItem), + props: { + type: "danger", + }, + label: t("core.common.buttons.delete"), + visible: true, + permissions: [], + action: handleDelete, + }, + ] +); diff --git a/console/src/modules/system/plugins/components/PluginListItem.vue b/console/src/modules/system/plugins/components/PluginListItem.vue index d88e0b4e0..d90d34869 100644 --- a/console/src/modules/system/plugins/components/PluginListItem.vue +++ b/console/src/modules/system/plugins/components/PluginListItem.vue @@ -8,19 +8,22 @@ import { Dialog, Toast, VDropdownItem, - VDropdown, VDropdownDivider, } from "@halo-dev/components"; -import { toRefs } from "vue"; +import { markRaw, toRefs } from "vue"; import { usePluginLifeCycle } from "../composables/use-plugin"; import type { Plugin } from "@halo-dev/api-client"; import { formatDatetime } from "@/utils/date"; import { usePermission } from "@/utils/permission"; import { apiClient } from "@/utils/api-client"; import { useI18n } from "vue-i18n"; +import { useEntityDropdownItemExtensionPoint } from "@/composables/use-entity-extension-points"; +import { useRouter } from "vue-router"; +import EntityDropdownItems from "@/components/entity/EntityDropdownItems.vue"; const { currentUserHasPermission } = usePermission(); const { t } = useI18n(); +const router = useRouter(); const props = withDefaults( defineProps<{ @@ -64,6 +67,83 @@ const handleResetSettingConfig = async () => { }, }); }; + +const { dropdownItems } = useEntityDropdownItemExtensionPoint( + "plugin:list-item:operation:create", + [ + { + priority: 10, + component: markRaw(VDropdownItem), + label: t("core.common.buttons.detail"), + visible: true, + permissions: [], + action: () => { + router.push({ + name: "PluginDetail", + params: { name: props.plugin?.metadata.name }, + }); + }, + }, + { + priority: 20, + component: markRaw(VDropdownItem), + label: t("core.common.buttons.upgrade"), + visible: true, + permissions: [], + action: () => { + emit("open-upgrade-modal", props.plugin); + }, + }, + { + priority: 30, + component: markRaw(VDropdownDivider), + visible: true, + }, + { + priority: 40, + component: markRaw(VDropdownItem), + props: { + type: "danger", + }, + label: t("core.common.buttons.uninstall"), + visible: true, + children: [ + { + priority: 10, + component: markRaw(VDropdownItem), + props: { + type: "danger", + }, + label: t("core.common.buttons.uninstall"), + visible: true, + action: () => uninstall(), + }, + { + priority: 20, + component: markRaw(VDropdownItem), + props: { + type: "danger", + }, + label: t("core.plugin.list.actions.uninstall_and_delete_config"), + visible: true, + action: () => uninstall(true), + }, + ], + }, + { + priority: 50, + component: markRaw(VDropdownItem), + props: { + type: "danger", + }, + label: t("core.common.buttons.reset"), + visible: true, + action: () => { + handleResetSettingConfig(); + }, + }, + ] +);