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/plugin-tab";
|
||||||
export * from "./states/comment-subject-ref";
|
export * from "./states/comment-subject-ref";
|
||||||
export * from "./states/backup";
|
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 { AnyExtension } from "@tiptap/vue-3";
|
||||||
import type { CommentSubjectRefProvider } from "@/states/comment-subject-ref";
|
import type { CommentSubjectRefProvider } from "@/states/comment-subject-ref";
|
||||||
import type { BackupTab } from "@/states/backup";
|
import type { BackupTab } from "@/states/backup";
|
||||||
|
import type { EntityDropdownItem } from "@/states/entity";
|
||||||
|
import type { ListedPost, Plugin } from "@halo-dev/api-client";
|
||||||
|
|
||||||
export interface RouteRecordAppend {
|
export interface RouteRecordAppend {
|
||||||
parentName: RouteRecordName;
|
parentName: RouteRecordName;
|
||||||
|
@ -31,6 +33,14 @@ export interface ExtensionPoint {
|
||||||
"comment:subject-ref:create"?: () => CommentSubjectRefProvider[];
|
"comment:subject-ref:create"?: () => CommentSubjectRefProvider[];
|
||||||
|
|
||||||
"backup:tabs:create"?: () => BackupTab[] | Promise<BackupTab[]>;
|
"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 {
|
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 type { Ref } from "vue";
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { computed } 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 { currentUserHasPermission } = usePermission();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -273,23 +323,7 @@ const handleDelete = async () => {
|
||||||
v-if="currentUserHasPermission(['system:posts:manage'])"
|
v-if="currentUserHasPermission(['system:posts:manage'])"
|
||||||
#dropdownItems
|
#dropdownItems
|
||||||
>
|
>
|
||||||
<VDropdownItem
|
<EntityDropdownItems :dropdown-items="dropdownItems" :item="post" />
|
||||||
@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>
|
|
||||||
</template>
|
</template>
|
||||||
</VEntity>
|
</VEntity>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -8,19 +8,22 @@ import {
|
||||||
Dialog,
|
Dialog,
|
||||||
Toast,
|
Toast,
|
||||||
VDropdownItem,
|
VDropdownItem,
|
||||||
VDropdown,
|
|
||||||
VDropdownDivider,
|
VDropdownDivider,
|
||||||
} from "@halo-dev/components";
|
} from "@halo-dev/components";
|
||||||
import { toRefs } from "vue";
|
import { markRaw, toRefs } from "vue";
|
||||||
import { usePluginLifeCycle } from "../composables/use-plugin";
|
import { usePluginLifeCycle } from "../composables/use-plugin";
|
||||||
import type { Plugin } from "@halo-dev/api-client";
|
import type { Plugin } from "@halo-dev/api-client";
|
||||||
import { formatDatetime } from "@/utils/date";
|
import { formatDatetime } from "@/utils/date";
|
||||||
import { usePermission } from "@/utils/permission";
|
import { usePermission } from "@/utils/permission";
|
||||||
import { apiClient } from "@/utils/api-client";
|
import { apiClient } from "@/utils/api-client";
|
||||||
import { useI18n } from "vue-i18n";
|
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 { currentUserHasPermission } = usePermission();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
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>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<VEntity>
|
<VEntity>
|
||||||
|
@ -135,36 +215,7 @@ const handleResetSettingConfig = async () => {
|
||||||
v-if="currentUserHasPermission(['system:plugins:manage'])"
|
v-if="currentUserHasPermission(['system:plugins:manage'])"
|
||||||
#dropdownItems
|
#dropdownItems
|
||||||
>
|
>
|
||||||
<VDropdownItem
|
<EntityDropdownItems :dropdown-items="dropdownItems" :item="plugin" />
|
||||||
@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>
|
|
||||||
</template>
|
</template>
|
||||||
</VEntity>
|
</VEntity>
|
||||||
</template>
|
</template>
|
||||||
|
|
Loading…
Reference in New Issue