Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add extension point for extend data list operation items #4452

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
68 changes: 68 additions & 0 deletions console/docs/extension-points/entity-listitem-operation.md
@@ -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>[]; // 子菜单
}
```
1 change: 1 addition & 0 deletions console/packages/shared/src/index.ts
Expand Up @@ -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";
12 changes: 12 additions & 0 deletions console/packages/shared/src/states/entity.ts
@@ -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>[];
}
10 changes: 10 additions & 0 deletions console/packages/shared/src/types/plugin.ts
Expand Up @@ -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;
Expand All @@ -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 {
Expand Down
66 changes: 66 additions & 0 deletions console/src/components/entity/EntityDropdownItems.vue
@@ -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>
36 changes: 36 additions & 0 deletions 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<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 };
}
68 changes: 51 additions & 17 deletions console/src/modules/contents/posts/components/PostListItem.vue
Expand Up @@ -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<{
Expand Down Expand Up @@ -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>
Expand Down Expand Up @@ -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>