feat: make comment subject ref provider extensible (#4039)

#### What type of PR is this?

/area console
/kind feature

#### What this PR does / why we need it:

让评论来源的显示支持通过插件扩展,目前如 [瞬间](https://github.com/halo-sigs/plugin-moments) 这类的插件如果使用了评论模块,那么在评论管理是无法显示具体来源的:

<img width="627" alt="image" src="https://github.com/halo-dev/halo/assets/21301288/0df354dc-ed42-4217-abbd-5bce67329e0d">

此 PR 为 Console 端提供了拓展方法,使用方式如下:

```ts
import { definePlugin } from "@halo-dev/console-shared";
import type { CommentSubjectRefResult } from "@halo-dev/console-shared";
import type { Extension } from "@halo-dev/api-client";
import type { Moment } from "./types";

export default definePlugin({
  components: {},
  extensionPoints: {
    "comment:subject-ref:create": () => {
      return [
        {
          kind: "Moment",
          group: "moment.halo.run",
          resolve: (subject: Extension): CommentSubjectRefResult => {
            const moment = subject as Moment;
            return {
              label: "瞬间",
              title: determineMomentTitle(moment),
              externalUrl: `/moments/${moment.metadata.name}`,
              route: {
                name: "Moments",
              },
            };
          },
        },
      ];
    },
  },
});

```

#### Which issue(s) this PR fixes:

Fixes #3554 

#### Special notes for your reviewer:

#### Does this PR introduce a user-facing change?

```release-note
Console 端的评论来源显示支持通过插件扩展
```
pull/4114/head^2
Ryan Wang 2023-06-26 12:20:18 +08:00 committed by GitHub
parent ac47942a04
commit 8c05a6d30e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 117 additions and 17 deletions

View File

@ -0,0 +1,59 @@
# 评论来源显示拓展点
在 Console 中,评论管理列表的评论来源默认仅支持显示来自文章和页面的评论,如果其他插件中的业务模块也使用了评论,那么就可以通过该拓展点来扩展评论来源的显示。
## 定义方式
假设以文章为例:
```ts
import { definePlugin } from "@halo-dev/console-shared";
import type { CommentSubjectRefResult } from "@halo-dev/console-shared";
import type { Extension } from "@halo-dev/api-client";
import type { Post } from "./types";
export default definePlugin({
components: {},
extensionPoints: {
"comment:subject-ref:create": () => {
return [
{
kind: "Post",
group: "post.halo.run",
resolve: (subject: Extension): CommentSubjectRefResult => {
const post = subject as Post;
return {
label: "文章",
title: post.spec.title,
externalUrl: post.status.permalink,
route: {
name: "PostEditor",
params: {
name: post.metadata.name
}
},
};
},
},
];
},
},
});
```
类型定义如下:
```ts
type CommentSubjectRefProvider = {
kind: string; // 自定义模型的类型
group: string; // 自定义模型的分组
resolve: (subject: Extension) => CommentSubjectRefResult;
}
interface CommentSubjectRefResult {
label: string; // 来源名称(类型)
title: string; // 来源标题
route?: RouteLocationRaw; // Console 的路由,可以设置为来源的详情或者编辑页面
externalUrl?: string; // 访问地址,可以设置为前台资源的访问地址
}
```

View File

@ -4,3 +4,4 @@ export * from "./core/plugins";
export * from "./states/pages";
export * from "./states/attachment-selector";
export * from "./states/editor";
export * from "./states/comment-subject-ref";

View File

@ -0,0 +1,15 @@
import type { Extension } from "@halo-dev/api-client";
import type { RouteLocationRaw } from "vue-router";
export interface CommentSubjectRefResult {
label: string;
title: string;
route?: RouteLocationRaw;
externalUrl?: string;
}
export type CommentSubjectRefProvider = {
kind: string;
group: string;
resolve: (subject: Extension) => CommentSubjectRefResult;
};

View File

@ -3,6 +3,7 @@ import type { RouteRecordRaw, RouteRecordName } from "vue-router";
import type { FunctionalPage } from "../states/pages";
import type { AttachmentSelectProvider } from "../states/attachment-selector";
import type { EditorProvider } from "..";
import type { CommentSubjectRefProvider } from "@/states/comment-subject-ref";
export interface RouteRecordAppend {
parentName: RouteRecordName;
@ -18,6 +19,8 @@ export interface ExtensionPoint {
| Promise<AttachmentSelectProvider[]>;
"editor:create"?: () => EditorProvider[] | Promise<EditorProvider[]>;
"comment:subject-ref:create"?: () => CommentSubjectRefProvider[];
}
export interface PluginModule {

View File

@ -24,14 +24,18 @@ import type {
SinglePage,
} from "@halo-dev/api-client";
import { formatDatetime } from "@/utils/date";
import { computed, provide, ref, type Ref } from "vue";
import { computed, provide, ref, onMounted, type Ref } from "vue";
import ReplyListItem from "./ReplyListItem.vue";
import { apiClient } from "@/utils/api-client";
import type { RouteLocationRaw } from "vue-router";
import cloneDeep from "lodash.clonedeep";
import { usePermission } from "@/utils/permission";
import { useQuery } from "@tanstack/vue-query";
import { useI18n } from "vue-i18n";
import { usePluginModuleStore, type PluginModule } from "@/stores/plugin";
import type {
CommentSubjectRefProvider,
CommentSubjectRefResult,
} from "packages/shared/dist";
const { currentUserHasPermission } = usePermission();
const { t } = useI18n();
@ -195,18 +199,11 @@ const onReplyCreationModalClose = () => {
};
// Subject ref processing
interface SubjectRefResult {
label: string;
title: string;
route?: RouteLocationRaw;
externalUrl?: string;
}
const SubjectRefProvider = ref<
Record<string, (subject: Extension) => SubjectRefResult>[]
>([
const SubjectRefProviders = ref<CommentSubjectRefProvider[]>([
{
Post: (subject: Extension): SubjectRefResult => {
kind: "Post",
group: "content.halo.run",
resolve: (subject: Extension): CommentSubjectRefResult => {
const post = subject as Post;
return {
label: t("core.comment.subject_refs.post"),
@ -222,7 +219,9 @@ const SubjectRefProvider = ref<
},
},
{
SinglePage: (subject: Extension): SubjectRefResult => {
kind: "SinglePage",
group: "content.halo.run",
resolve: (subject: Extension): CommentSubjectRefResult => {
const singlePage = subject as SinglePage;
return {
label: t("core.comment.subject_refs.page"),
@ -239,6 +238,27 @@ const SubjectRefProvider = ref<
},
]);
onMounted(() => {
const { pluginModules } = usePluginModuleStore();
pluginModules.forEach((pluginModule: PluginModule) => {
const { extensionPoints } = pluginModule;
if (!extensionPoints?.["comment:subject-ref:create"]) {
return;
}
const providers = extensionPoints[
"comment:subject-ref:create"
]() as CommentSubjectRefProvider[];
if (providers) {
providers.forEach((provider) => {
SubjectRefProviders.value.push(provider);
});
}
});
});
const subjectRefResult = computed(() => {
const { subject } = props.comment;
if (!subject) {
@ -247,8 +267,10 @@ const subjectRefResult = computed(() => {
title: t("core.comment.subject_refs.unknown"),
};
}
const subjectRef = SubjectRefProvider.value.find((provider) =>
Object.keys(provider).includes(subject.kind)
const subjectRef = SubjectRefProviders.value.find(
(provider) =>
provider.kind === subject.kind &&
subject.apiVersion.startsWith(provider.group)
);
if (!subjectRef) {
return {
@ -256,7 +278,7 @@ const subjectRefResult = computed(() => {
title: t("core.comment.subject_refs.unknown"),
};
}
return subjectRef[subject.kind](subject);
return subjectRef.resolve(subject);
});
</script>