mirror of https://github.com/halo-dev/halo
feat: add extension point for extend data list operation items (#4452)
#### 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 文档。 <img width="374" alt="image" src="https://github.com/halo-dev/halo/assets/21301288/eb0b7c61-f5c8-4af3-bf13-579681d36097"> #### Does this PR introduce a user-facing change? ```release-note Console 端的文章和插件列表的操作按钮列表支持扩展。 ```pull/4461/head^2
parent
acb738a8ac
commit
70eb039468
|
@ -0,0 +1,68 @@
|
|||
# Entity 数据列表操作菜单扩展点
|
||||
|
||||
## 原由
|
||||
|
||||
目前 Halo 2 的 Console 中,展示数据列表是统一使用 Entity 组件,Entity 组件中提供了用于放置操作按钮的插槽,此扩展点用于支持通过插件扩展部分数据列表的操作菜单项。
|
||||
|
||||
## 定义方式
|
||||
|
||||
目前支持扩展的数据列表:
|
||||
|
||||
- 文章:`"post:list-item:operation:create"?: () => | EntityDropdownItem<ListedPost>[] | Promise<EntityDropdownItem<ListedPost>[]>`
|
||||
- 插件:`"plugin:list-item:operation:create"?: () => | EntityDropdownItem<Plugin>[] | Promise<EntityDropdownItem<Plugin>[]>`
|
||||
|
||||
示例:
|
||||
|
||||
> 此示例是在文章列表中添加一个`导出为 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<T> {
|
||||
priority: number; // 优先级,越小越靠前
|
||||
component: Raw<Component>; // 菜单项组件,可以使用 `@halo-dev/components` 中提供的 `VDropdownItem`,也可以自定义
|
||||
props?: Record<string, unknown>; // 组件的 props
|
||||
action?: (item?: T) => void; // 点击事件
|
||||
label?: string; // 菜单项名称
|
||||
visible?: boolean; // 是否可见
|
||||
permissions?: string[]; // 权限
|
||||
children?: EntityDropdownItem<T>[]; // 子菜单
|
||||
}
|
||||
```
|
|
@ -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";
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
import type { Component, Raw } from "vue";
|
||||
|
||||
export interface EntityDropdownItem<T> {
|
||||
priority: number;
|
||||
component: Raw<Component>;
|
||||
props?: Record<string, unknown>;
|
||||
action?: (item?: T) => void;
|
||||
label?: string;
|
||||
visible?: boolean;
|
||||
permissions?: string[];
|
||||
children?: EntityDropdownItem<T>[];
|
||||
}
|
|
@ -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<BackupTab[]>;
|
||||
|
||||
"post:list-item:operation:create"?: () =>
|
||||
| EntityDropdownItem<ListedPost>[]
|
||||
| Promise<EntityDropdownItem<ListedPost>[]>;
|
||||
|
||||
"plugin:list-item:operation:create"?: () =>
|
||||
| EntityDropdownItem<Plugin>[]
|
||||
| Promise<EntityDropdownItem<Plugin>[]>;
|
||||
}
|
||||
|
||||
export interface PluginModule {
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
<script setup lang="ts" generic="T">
|
||||
import type { EntityDropdownItem } from "@halo-dev/console-shared";
|
||||
import { VDropdown } from "@halo-dev/components";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
dropdownItems: EntityDropdownItem<T>[];
|
||||
item?: T;
|
||||
}>(),
|
||||
{
|
||||
item: undefined,
|
||||
}
|
||||
);
|
||||
|
||||
function action(dropdownItem: EntityDropdownItem<T>) {
|
||||
if (!dropdownItem.action) {
|
||||
return;
|
||||
}
|
||||
dropdownItem.action(props.item);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template v-for="(dropdownItem, index) in dropdownItems">
|
||||
<template v-if="dropdownItem.visible">
|
||||
<VDropdown
|
||||
v-if="dropdownItem.children?.length"
|
||||
:key="`dropdown-children-items-${index}`"
|
||||
v-permission="dropdownItem.permissions"
|
||||
:triggers="['click']"
|
||||
>
|
||||
<component
|
||||
:is="dropdownItem.component"
|
||||
v-bind="dropdownItem.props"
|
||||
@click="action(dropdownItem)"
|
||||
>
|
||||
{{ dropdownItem.label }}
|
||||
</component>
|
||||
<template #popper>
|
||||
<template v-for="(childItem, childIndex) in dropdownItem.children">
|
||||
<component
|
||||
:is="childItem.component"
|
||||
v-if="childItem.visible"
|
||||
v-bind="childItem.props"
|
||||
:key="`dropdown-child-item-${childIndex}`"
|
||||
v-permission="childItem.permissions"
|
||||
@click="action(childItem)"
|
||||
>
|
||||
{{ childItem.label }}
|
||||
</component>
|
||||
</template>
|
||||
</template>
|
||||
</VDropdown>
|
||||
<component
|
||||
:is="dropdownItem.component"
|
||||
v-else
|
||||
v-bind="dropdownItem.props"
|
||||
:key="`dropdown-item-${index}`"
|
||||
v-permission="dropdownItem.permissions"
|
||||
@click="action(dropdownItem)"
|
||||
>
|
||||
{{ dropdownItem.label }}
|
||||
</component>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
|
@ -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<T>(
|
||||
extensionPointName: string,
|
||||
presets: EntityDropdownItem<T>[]
|
||||
) {
|
||||
const { pluginModules } = usePluginModuleStore();
|
||||
|
||||
const dropdownItems = ref<EntityDropdownItem<T>[]>(presets);
|
||||
|
||||
onMounted(() => {
|
||||
pluginModules.forEach((pluginModule: PluginModule) => {
|
||||
const { extensionPoints } = pluginModule;
|
||||
if (!extensionPoints?.[extensionPointName]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const items = extensionPoints[
|
||||
extensionPointName
|
||||
]() as EntityDropdownItem<T>[];
|
||||
|
||||
dropdownItems.value.push(...items);
|
||||
});
|
||||
|
||||
dropdownItems.value.sort((a, b) => {
|
||||
return a.priority - b.priority;
|
||||
});
|
||||
});
|
||||
|
||||
return { dropdownItems };
|
||||
}
|
|
@ -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<ListedPost>(
|
||||
"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,
|
||||
},
|
||||
]
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -273,23 +323,7 @@ const handleDelete = async () => {
|
|||
v-if="currentUserHasPermission(['system:posts:manage'])"
|
||||
#dropdownItems
|
||||
>
|
||||
<VDropdownItem
|
||||
@click="
|
||||
$router.push({
|
||||
name: 'PostEditor',
|
||||
query: { name: post.post.metadata.name },
|
||||
})
|
||||
"
|
||||
>
|
||||
{{ $t("core.common.buttons.edit") }}
|
||||
</VDropdownItem>
|
||||
<VDropdownItem @click="emit('open-setting-modal', post.post)">
|
||||
{{ $t("core.common.buttons.setting") }}
|
||||
</VDropdownItem>
|
||||
<VDropdownDivider />
|
||||
<VDropdownItem type="danger" @click="handleDelete">
|
||||
{{ $t("core.common.buttons.delete") }}
|
||||
</VDropdownItem>
|
||||
<EntityDropdownItems :dropdown-items="dropdownItems" :item="post" />
|
||||
</template>
|
||||
</VEntity>
|
||||
</template>
|
||||
|
|
|
@ -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>(
|
||||
"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();
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<VEntity>
|
||||
|
@ -135,36 +215,7 @@ const handleResetSettingConfig = async () => {
|
|||
v-if="currentUserHasPermission(['system:plugins:manage'])"
|
||||
#dropdownItems
|
||||
>
|
||||
<VDropdownItem
|
||||
@click="
|
||||
$router.push({
|
||||
name: 'PluginDetail',
|
||||
params: { name: plugin?.metadata.name },
|
||||
})
|
||||
"
|
||||
>
|
||||
{{ $t("core.common.buttons.detail") }}
|
||||
</VDropdownItem>
|
||||
<VDropdownItem @click="emit('open-upgrade-modal', plugin)">
|
||||
{{ $t("core.common.buttons.upgrade") }}
|
||||
</VDropdownItem>
|
||||
<VDropdownDivider />
|
||||
<VDropdown placement="left" :triggers="['click']">
|
||||
<VDropdownItem type="danger">
|
||||
{{ $t("core.common.buttons.uninstall") }}
|
||||
</VDropdownItem>
|
||||
<template #popper>
|
||||
<VDropdownItem type="danger" @click="uninstall">
|
||||
{{ $t("core.common.buttons.uninstall") }}
|
||||
</VDropdownItem>
|
||||
<VDropdownItem type="danger" @click="uninstall(true)">
|
||||
{{ $t("core.plugin.list.actions.uninstall_and_delete_config") }}
|
||||
</VDropdownItem>
|
||||
</template>
|
||||
</VDropdown>
|
||||
<VDropdownItem type="danger" @click="handleResetSettingConfig">
|
||||
{{ $t("core.common.buttons.reset") }}
|
||||
</VDropdownItem>
|
||||
<EntityDropdownItems :dropdown-items="dropdownItems" :item="plugin" />
|
||||
</template>
|
||||
</VEntity>
|
||||
</template>
|
||||
|
|
Loading…
Reference in New Issue