feat: enable asynchronous resolving for UI extension points (#6018)

#### What type of PR is this?

/area ui
/kind feature
/milestone 2.16.x

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

优化 UI 部分的扩展点获取实现,让部分扩展点支持异步获取,之前的实现与文档不符。

比如:

```ts
import { definePlugin } from "@halo-dev/console-shared";
import axios from "axios";

export default definePlugin({
  components: {},
  routes: [],
  extensionPoints: {
    "attachment:selector:create": async () => {
      const { data } = await axios.get(
        "/apis/v1alpha1/fake.halo.run/attachments/selectors"
      );

      return data;
    },
  },
});
```

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

Fixes https://github.com/halo-dev/halo/issues/6008

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

```release-note
优化 UI 部分的扩展点获取实现,让部分扩展点支持异步获取。
```
pull/6022/head
Ryan Wang 2024-05-30 15:01:15 +08:00 committed by GitHub
parent bf75a36df7
commit 4c6abdcaa1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 300 additions and 282 deletions

View File

@ -1,14 +1,13 @@
<script lang="ts" setup> <script lang="ts" setup>
import { usePluginModuleStore } from "@/stores/plugin";
import { VButton, VModal, VSpace, VTabbar } from "@halo-dev/components"; import { VButton, VModal, VSpace, VTabbar } from "@halo-dev/components";
import { ref, markRaw, onMounted, computed } from "vue";
import CoreSelectorProvider from "./selector-providers/CoreSelectorProvider.vue";
import type { import type {
AttachmentLike, AttachmentLike,
AttachmentSelectProvider, AttachmentSelectProvider,
PluginModule,
} from "@halo-dev/console-shared"; } from "@halo-dev/console-shared";
import { usePluginModuleStore } from "@/stores/plugin"; import { computed, markRaw, onMounted, ref } from "vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import CoreSelectorProvider from "./selector-providers/CoreSelectorProvider.vue";
const { t } = useI18n(); const { t } = useI18n();
@ -46,23 +45,22 @@ const attachmentSelectProviders = ref<AttachmentSelectProvider[]>([
// resolve plugin extension points // resolve plugin extension points
const { pluginModules } = usePluginModuleStore(); const { pluginModules } = usePluginModuleStore();
onMounted(() => { onMounted(async () => {
pluginModules.forEach((pluginModule: PluginModule) => { for (const pluginModule of pluginModules) {
const { extensionPoints } = pluginModule; try {
if (!extensionPoints?.["attachment:selector:create"]) { const callbackFunction =
return; pluginModule?.extensionPoints?.["attachment:selector:create"];
}
const providers = extensionPoints[ if (typeof callbackFunction !== "function") {
"attachment:selector:create" continue;
]() as AttachmentSelectProvider[]; }
if (providers) { const providers = await callbackFunction();
providers.forEach((provider) => { attachmentSelectProviders.value.push(...providers);
attachmentSelectProviders.value.push(provider); } catch (error) {
}); console.error(`Error processing plugin module:`, pluginModule, error);
} }
}); }
}); });
const activeId = ref(attachmentSelectProviders.value[0].id); const activeId = ref(attachmentSelectProviders.value[0].id);

View File

@ -1,4 +1,15 @@
<script lang="ts" setup> <script lang="ts" setup>
import { usePluginModuleStore } from "@/stores/plugin";
import { apiClient } from "@/utils/api-client";
import { formatDatetime } from "@/utils/date";
import { usePermission } from "@/utils/permission";
import type {
Extension,
ListedComment,
ListedReply,
Post,
SinglePage,
} from "@halo-dev/api-client";
import { import {
Dialog, Dialog,
IconAddCircle, IconAddCircle,
@ -15,28 +26,16 @@ import {
VStatusDot, VStatusDot,
VTag, VTag,
} from "@halo-dev/components"; } from "@halo-dev/components";
import ReplyCreationModal from "./ReplyCreationModal.vue";
import type {
Extension,
ListedComment,
ListedReply,
Post,
SinglePage,
} from "@halo-dev/api-client";
import { formatDatetime } from "@/utils/date";
import { computed, onMounted, provide, ref, type Ref } from "vue";
import ReplyListItem from "./ReplyListItem.vue";
import { apiClient } from "@/utils/api-client";
import { cloneDeep } from "lodash-es";
import { usePermission } from "@/utils/permission";
import { useMutation, useQuery, useQueryClient } from "@tanstack/vue-query";
import { useI18n } from "vue-i18n";
import { usePluginModuleStore } from "@/stores/plugin";
import type { import type {
CommentSubjectRefProvider, CommentSubjectRefProvider,
CommentSubjectRefResult, CommentSubjectRefResult,
PluginModule,
} from "@halo-dev/console-shared"; } from "@halo-dev/console-shared";
import { useMutation, useQuery, useQueryClient } from "@tanstack/vue-query";
import { cloneDeep } from "lodash-es";
import { computed, onMounted, provide, ref, type Ref } from "vue";
import { useI18n } from "vue-i18n";
import ReplyCreationModal from "./ReplyCreationModal.vue";
import ReplyListItem from "./ReplyListItem.vue";
const { currentUserHasPermission } = usePermission(); const { currentUserHasPermission } = usePermission();
const { t } = useI18n(); const { t } = useI18n();
@ -250,25 +249,21 @@ const SubjectRefProviders = ref<CommentSubjectRefProvider[]>([
}, },
]); ]);
const { pluginModules } = usePluginModuleStore();
onMounted(() => { onMounted(() => {
const { pluginModules } = usePluginModuleStore(); for (const pluginModule of pluginModules) {
const callbackFunction =
pluginModule?.extensionPoints?.["comment:subject-ref:create"];
pluginModules.forEach((pluginModule: PluginModule) => { if (typeof callbackFunction !== "function") {
const { extensionPoints } = pluginModule; continue;
if (!extensionPoints?.["comment:subject-ref:create"]) {
return;
} }
const providers = extensionPoints[ const providers = callbackFunction();
"comment:subject-ref:create"
]() as CommentSubjectRefProvider[];
if (providers) { SubjectRefProviders.value.push(...providers);
providers.forEach((provider) => { }
SubjectRefProviders.value.push(provider);
});
}
});
}); });
const subjectRefResult = computed(() => { const subjectRefResult = computed(() => {

View File

@ -1,5 +1,10 @@
<script lang="ts" setup> <script lang="ts" setup>
import { usePluginModuleStore } from "@/stores/plugin";
import { usePermission } from "@/utils/permission";
import type { Theme } from "@halo-dev/api-client";
import { VButton, VModal, VTabbar } from "@halo-dev/components"; import { VButton, VModal, VTabbar } from "@halo-dev/components";
import type { ThemeListTab } from "@halo-dev/console-shared";
import { useRouteQuery } from "@vueuse/router";
import { import {
computed, computed,
inject, inject,
@ -8,19 +13,14 @@ import {
onMounted, onMounted,
provide, provide,
ref, ref,
type Ref,
watch, watch,
type Ref,
} from "vue"; } from "vue";
import type { Theme } from "@halo-dev/api-client";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { useRouteQuery } from "@vueuse/router";
import InstalledThemes from "./list-tabs/InstalledThemes.vue"; import InstalledThemes from "./list-tabs/InstalledThemes.vue";
import NotInstalledThemes from "./list-tabs/NotInstalledThemes.vue";
import LocalUpload from "./list-tabs/LocalUpload.vue"; import LocalUpload from "./list-tabs/LocalUpload.vue";
import NotInstalledThemes from "./list-tabs/NotInstalledThemes.vue";
import RemoteDownload from "./list-tabs/RemoteDownload.vue"; import RemoteDownload from "./list-tabs/RemoteDownload.vue";
import { usePluginModuleStore } from "@/stores/plugin";
import type { PluginModule, ThemeListTab } from "@halo-dev/console-shared";
import { usePermission } from "@/utils/permission";
const { t } = useI18n(); const { t } = useI18n();
const { currentUserHasPermission } = usePermission(); const { currentUserHasPermission } = usePermission();
@ -92,22 +92,30 @@ onMounted(() => {
}); });
const { pluginModules } = usePluginModuleStore(); const { pluginModules } = usePluginModuleStore();
onMounted(() => {
onMounted(async () => {
const tabsFromPlugins: ThemeListTab[] = []; const tabsFromPlugins: ThemeListTab[] = [];
pluginModules.forEach((pluginModule: PluginModule) => {
const { extensionPoints } = pluginModule; for (const pluginModule of pluginModules) {
if (!extensionPoints?.["theme:list:tabs:create"]) { try {
return; const callbackFunction =
pluginModule?.extensionPoints?.["theme:list:tabs:create"];
if (typeof callbackFunction !== "function") {
continue;
}
const items = await callbackFunction();
tabsFromPlugins.push(
...items.filter((item) => {
return currentUserHasPermission(item.permissions);
})
);
} catch (error) {
console.error(`Error processing plugin module:`, pluginModule, error);
} }
}
let items = extensionPoints["theme:list:tabs:create"]() as ThemeListTab[];
items = items.filter((item) => {
return currentUserHasPermission(item.permissions);
});
tabsFromPlugins.push(...items);
});
tabs.value = tabs.value.concat(tabsFromPlugins).sort((a, b) => { tabs.value = tabs.value.concat(tabsFromPlugins).sort((a, b) => {
return a.priority - b.priority; return a.priority - b.priority;

View File

@ -1,22 +1,21 @@
<script lang="ts" setup> <script lang="ts" setup>
import { import {
VPageHeader,
VCard,
VButton,
VTabbar,
IconAddCircle, IconAddCircle,
IconServerLine, IconServerLine,
VButton,
VCard,
VPageHeader,
VTabbar,
} from "@halo-dev/components"; } from "@halo-dev/components";
import { onMounted, shallowRef } from "vue";
import ListTab from "./tabs/List.vue";
import RestoreTab from "./tabs/Restore.vue";
import { useRouteQuery } from "@vueuse/router";
import { markRaw } from "vue";
import { useI18n } from "vue-i18n";
import { useBackup } from "./composables/use-backup";
import { usePluginModuleStore } from "@/stores/plugin"; import { usePluginModuleStore } from "@/stores/plugin";
import type { BackupTab } from "@halo-dev/console-shared"; import type { BackupTab } from "@halo-dev/console-shared";
import { useRouteQuery } from "@vueuse/router";
import { markRaw, onMounted, shallowRef } from "vue";
import { useI18n } from "vue-i18n";
import { useBackup } from "./composables/use-backup";
import ListTab from "./tabs/List.vue";
import RestoreTab from "./tabs/Restore.vue";
const { t } = useI18n(); const { t } = useI18n();
@ -37,21 +36,27 @@ const activeTab = useRouteQuery<string>("tab", tabs.value[0].id);
const { handleCreate } = useBackup(); const { handleCreate } = useBackup();
onMounted(() => { const { pluginModules } = usePluginModuleStore();
const { pluginModules } = usePluginModuleStore();
pluginModules.forEach((pluginModule) => { onMounted(async () => {
const { extensionPoints } = pluginModule; for (const pluginModule of pluginModules) {
if (!extensionPoints?.["backup:tabs:create"]) { try {
return; const callbackFunction =
pluginModule?.extensionPoints?.["backup:tabs:create"];
if (typeof callbackFunction !== "function") {
continue;
}
const backupTabs = await callbackFunction();
if (backupTabs) {
tabs.value = tabs.value.concat(backupTabs);
}
} catch (error) {
console.error(`Error processing plugin module:`, pluginModule, error);
} }
}
const backupTabs = extensionPoints["backup:tabs:create"]() as BackupTab[];
if (backupTabs) {
tabs.value = tabs.value.concat(backupTabs);
}
});
}); });
</script> </script>

View File

@ -1,27 +1,27 @@
<script lang="ts" setup> <script lang="ts" setup>
// core libs // core libs
import { provide, ref, computed } from "vue";
import { useRoute } from "vue-router";
import { apiClient } from "@/utils/api-client"; import { apiClient } from "@/utils/api-client";
import { computed, provide, ref } from "vue";
import { useRoute } from "vue-router";
// libs // libs
import { cloneDeep } from "lodash-es"; import { cloneDeep } from "lodash-es";
// components // components
import { VCard, VPageHeader, VTabbar, VAvatar } from "@halo-dev/components"; import { VAvatar, VCard, VPageHeader, VTabbar } from "@halo-dev/components";
// types // types
import type { Ref } from "vue"; import { usePluginModuleStore } from "@/stores/plugin";
import type { Plugin, Setting, SettingForm } from "@halo-dev/api-client";
import { usePermission } from "@/utils/permission"; import { usePermission } from "@/utils/permission";
import { useI18n } from "vue-i18n"; import type { Plugin, Setting, SettingForm } from "@halo-dev/api-client";
import { useQuery } from "@tanstack/vue-query";
import type { PluginTab } from "@halo-dev/console-shared"; import type { PluginTab } from "@halo-dev/console-shared";
import { useQuery } from "@tanstack/vue-query";
import { useRouteQuery } from "@vueuse/router";
import type { Ref } from "vue";
import { markRaw } from "vue"; import { markRaw } from "vue";
import { useI18n } from "vue-i18n";
import DetailTab from "./tabs/Detail.vue"; import DetailTab from "./tabs/Detail.vue";
import SettingTab from "./tabs/Setting.vue"; import SettingTab from "./tabs/Setting.vue";
import { useRouteQuery } from "@vueuse/router";
import { usePluginModuleStore } from "@/stores/plugin";
const { currentUserHasPermission } = usePermission(); const { currentUserHasPermission } = usePermission();
const { t } = useI18n(); const { t } = useI18n();
@ -50,12 +50,12 @@ const { data: plugin } = useQuery({
}); });
return data; return data;
}, },
onSuccess(data) { async onSuccess(data) {
if ( if (
!data.spec.settingName || !data.spec.settingName ||
!currentUserHasPermission(["system:plugins:manage"]) !currentUserHasPermission(["system:plugins:manage"])
) { ) {
tabs.value = [...initialTabs.value, ...getTabsFromExtensions()]; tabs.value = [...initialTabs.value, ...(await getTabsFromExtensions())];
} }
}, },
}); });
@ -82,7 +82,7 @@ const { data: setting } = useQuery({
const { forms } = data.spec; const { forms } = data.spec;
tabs.value = [ tabs.value = [
...initialTabs.value, ...initialTabs.value,
...getTabsFromExtensions(), ...(await getTabsFromExtensions()),
...forms.map((item: SettingForm) => { ...forms.map((item: SettingForm) => {
return { return {
id: item.group, id: item.group,
@ -97,7 +97,7 @@ const { data: setting } = useQuery({
provide<Ref<Setting | undefined>>("setting", setting); provide<Ref<Setting | undefined>>("setting", setting);
function getTabsFromExtensions(): PluginTab[] { async function getTabsFromExtensions() {
const { pluginModuleMap } = usePluginModuleStore(); const { pluginModuleMap } = usePluginModuleStore();
const currentPluginModule = pluginModuleMap[route.params.name as string]; const currentPluginModule = pluginModuleMap[route.params.name as string];
@ -106,15 +106,14 @@ function getTabsFromExtensions(): PluginTab[] {
return []; return [];
} }
const { extensionPoints } = currentPluginModule; const callbackFunction =
currentPluginModule?.extensionPoints?.["plugin:self:tabs:create"];
if (!extensionPoints?.["plugin:self:tabs:create"]) { if (typeof callbackFunction !== "function") {
return []; return [];
} }
const pluginTabs = extensionPoints[ const pluginTabs = await callbackFunction();
"plugin:self:tabs:create"
]() as PluginTab[];
return pluginTabs.filter((tab) => { return pluginTabs.filter((tab) => {
return currentUserHasPermission(tab.permissions); return currentUserHasPermission(tab.permissions);

View File

@ -1,26 +1,23 @@
<script lang="ts" setup> <script lang="ts" setup>
import { VButton, VModal, VTabbar } from "@halo-dev/components"; import { usePluginModuleStore } from "@/stores/plugin";
import { usePermission } from "@/utils/permission";
import type { Plugin } from "@halo-dev/api-client"; import type { Plugin } from "@halo-dev/api-client";
import { VButton, VModal, VTabbar } from "@halo-dev/components";
import type { PluginInstallationTab } from "@halo-dev/console-shared";
import { useRouteQuery } from "@vueuse/router";
import { import {
computed, computed,
markRaw, markRaw,
nextTick, nextTick,
onMounted, onMounted,
provide, provide,
type Ref,
ref, ref,
toRefs, toRefs,
type Ref,
} from "vue"; } from "vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { useRouteQuery } from "@vueuse/router";
import LocalUpload from "./installation-tabs/LocalUpload.vue"; import LocalUpload from "./installation-tabs/LocalUpload.vue";
import RemoteDownload from "./installation-tabs/RemoteDownload.vue"; import RemoteDownload from "./installation-tabs/RemoteDownload.vue";
import type {
PluginInstallationTab,
PluginModule,
} from "@halo-dev/console-shared";
import { usePluginModuleStore } from "@/stores/plugin";
import { usePermission } from "@/utils/permission";
const { t } = useI18n(); const { t } = useI18n();
const { currentUserHasPermission } = usePermission(); const { currentUserHasPermission } = usePermission();
@ -82,23 +79,28 @@ onMounted(() => {
}); });
const { pluginModules } = usePluginModuleStore(); const { pluginModules } = usePluginModuleStore();
onMounted(() => {
pluginModules.forEach((pluginModule: PluginModule) => { onMounted(async () => {
const { extensionPoints } = pluginModule; for (const pluginModule of pluginModules) {
if (!extensionPoints?.["plugin:installation:tabs:create"]) { try {
return; const callbackFunction =
pluginModule?.extensionPoints?.["plugin:installation:tabs:create"];
if (typeof callbackFunction !== "function") {
continue;
}
const items = await callbackFunction();
tabs.value.push(
...items.filter((item) => {
return currentUserHasPermission(item.permissions);
})
);
} catch (error) {
console.error(`Error processing plugin module:`, pluginModule, error);
} }
}
let items = extensionPoints[
"plugin:installation:tabs:create"
]() as PluginInstallationTab[];
items = items.filter((item) => {
return currentUserHasPermission(item.permissions);
});
tabs.value.push(...items);
});
tabs.value.sort((a, b) => { tabs.value.sort((a, b) => {
return a.priority - b.priority; return a.priority - b.priority;

View File

@ -1,32 +1,32 @@
<script lang="ts" setup> <script lang="ts" setup>
import UserAvatar from "@/components/user-avatar/UserAvatar.vue";
import { usePluginModuleStore } from "@/stores/plugin";
import { useUserStore } from "@/stores/user";
import { apiClient } from "@/utils/api-client"; import { apiClient } from "@/utils/api-client";
import { usePermission } from "@/utils/permission";
import { import {
VButton, VButton,
VDropdown, VDropdown,
VDropdownItem, VDropdownItem,
VTabbar, VTabbar,
} from "@halo-dev/components"; } from "@halo-dev/components";
import type { UserTab } from "@halo-dev/console-shared";
import { useQuery } from "@tanstack/vue-query";
import { useRouteQuery } from "@vueuse/router";
import { import {
computed, computed,
markRaw, markRaw,
onMounted, onMounted,
provide, provide,
type Ref,
ref, ref,
toRaw, toRaw,
type Ref,
} from "vue"; } from "vue";
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router"; import { useRoute } from "vue-router";
import UserEditingModal from "./components/UserEditingModal.vue"; import UserEditingModal from "./components/UserEditingModal.vue";
import UserPasswordChangeModal from "./components/UserPasswordChangeModal.vue"; import UserPasswordChangeModal from "./components/UserPasswordChangeModal.vue";
import { usePermission } from "@/utils/permission";
import { useQuery } from "@tanstack/vue-query";
import { useI18n } from "vue-i18n";
import UserAvatar from "@/components/user-avatar/UserAvatar.vue";
import DetailTab from "./tabs/Detail.vue"; import DetailTab from "./tabs/Detail.vue";
import { useRouteQuery } from "@vueuse/router";
import { useUserStore } from "@/stores/user";
import { usePluginModuleStore } from "@/stores/plugin";
import type { PluginModule, UserTab } from "@halo-dev/console-shared";
const { currentUserHasPermission } = usePermission(); const { currentUserHasPermission } = usePermission();
const { t } = useI18n(); const { t } = useI18n();
@ -62,19 +62,24 @@ const tabs = ref<UserTab[]>([
]); ]);
// Collect user:detail:tabs:create extension points // Collect user:detail:tabs:create extension points
onMounted(() => { const { pluginModules } = usePluginModuleStore();
const { pluginModules } = usePluginModuleStore();
pluginModules.forEach((pluginModule: PluginModule) => { onMounted(async () => {
const { extensionPoints } = pluginModule; for (const pluginModule of pluginModules) {
if (!extensionPoints?.["user:detail:tabs:create"]) { try {
return; const callbackFunction =
pluginModule?.extensionPoints?.["user:detail:tabs:create"];
if (typeof callbackFunction !== "function") {
continue;
}
const providers = await callbackFunction();
tabs.value.push(...providers);
} catch (error) {
console.error(`Error processing plugin module:`, pluginModule, error);
} }
}
const providers = extensionPoints["user:detail:tabs:create"]() as UserTab[];
tabs.value.push(...providers);
});
}); });
const activeTab = useRouteQuery<string>("tab", tabs.value[0].id, { const activeTab = useRouteQuery<string>("tab", tabs.value[0].id, {

View File

@ -1,15 +1,10 @@
import type { Component, Ref } from "vue";
import type { RouteRecordName, RouteRecordRaw } from "vue-router";
import type { FunctionalPage } from "../states/pages";
import type { AttachmentSelectProvider } from "../states/attachment-selector";
import type { EditorProvider, PluginTab } from "..";
import type { AnyExtension } from "@halo-dev/richtext-editor";
import type { CommentSubjectRefProvider } from "@/states/comment-subject-ref";
import type { BackupTab } from "@/states/backup"; import type { BackupTab } from "@/states/backup";
import type { PluginInstallationTab } from "@/states/plugin-installation-tabs"; import type { CommentSubjectRefProvider } from "@/states/comment-subject-ref";
import type { EntityFieldItem } from "@/states/entity"; import type { EntityFieldItem } from "@/states/entity";
import type { OperationItem } from "@/states/operation"; import type { OperationItem } from "@/states/operation";
import type { PluginInstallationTab } from "@/states/plugin-installation-tabs";
import type { ThemeListTab } from "@/states/theme-list-tabs"; import type { ThemeListTab } from "@/states/theme-list-tabs";
import type { UserProfileTab, UserTab } from "@/states/user-tab";
import type { import type {
Attachment, Attachment,
Backup, Backup,
@ -17,7 +12,12 @@ import type {
Plugin, Plugin,
Theme, Theme,
} from "@halo-dev/api-client"; } from "@halo-dev/api-client";
import type { UserProfileTab, UserTab } from "@/states/user-tab"; import type { AnyExtension } from "@halo-dev/richtext-editor";
import type { Component, Ref } from "vue";
import type { RouteRecordName, RouteRecordRaw } from "vue-router";
import type { EditorProvider, PluginTab } from "..";
import type { AttachmentSelectProvider } from "../states/attachment-selector";
import type { FunctionalPage } from "../states/pages";
export interface RouteRecordAppend { export interface RouteRecordAppend {
parentName: RouteRecordName; parentName: RouteRecordName;
@ -50,33 +50,29 @@ export interface ExtensionPoint {
"post:list-item:operation:create"?: ( "post:list-item:operation:create"?: (
post: Ref<ListedPost> post: Ref<ListedPost>
) => OperationItem<ListedPost>[] | Promise<OperationItem<ListedPost>[]>; ) => OperationItem<ListedPost>[];
"plugin:list-item:operation:create"?: ( "plugin:list-item:operation:create"?: (
plugin: Ref<Plugin> plugin: Ref<Plugin>
) => OperationItem<Plugin>[] | Promise<OperationItem<Plugin>[]>; ) => OperationItem<Plugin>[];
"backup:list-item:operation:create"?: ( "backup:list-item:operation:create"?: (
backup: Ref<Backup> backup: Ref<Backup>
) => OperationItem<Backup>[] | Promise<OperationItem<Backup>[]>; ) => OperationItem<Backup>[];
"attachment:list-item:operation:create"?: ( "attachment:list-item:operation:create"?: (
attachment: Ref<Attachment> attachment: Ref<Attachment>
) => OperationItem<Attachment>[] | Promise<OperationItem<Attachment>[]>; ) => OperationItem<Attachment>[];
"plugin:list-item:field:create"?: ( "plugin:list-item:field:create"?: (plugin: Ref<Plugin>) => EntityFieldItem[];
plugin: Ref<Plugin>
) => EntityFieldItem[] | Promise<EntityFieldItem[]>;
"post:list-item:field:create"?: ( "post:list-item:field:create"?: (post: Ref<ListedPost>) => EntityFieldItem[];
post: Ref<ListedPost>
) => EntityFieldItem[] | Promise<EntityFieldItem[]>;
"theme:list:tabs:create"?: () => ThemeListTab[] | Promise<ThemeListTab[]>; "theme:list:tabs:create"?: () => ThemeListTab[] | Promise<ThemeListTab[]>;
"theme:list-item:operation:create"?: ( "theme:list-item:operation:create"?: (
theme: Ref<Theme> theme: Ref<Theme>
) => OperationItem<Theme>[] | Promise<OperationItem<Theme>[]>; ) => OperationItem<Theme>[];
"user:detail:tabs:create"?: () => UserTab[] | Promise<UserTab[]>; "user:detail:tabs:create"?: () => UserTab[] | Promise<UserTab[]>;

View File

@ -1,62 +1,62 @@
<script lang="ts" setup> <script lang="ts" setup>
import { import {
DecorationSet,
Editor,
Extension, Extension,
RichTextEditor,
ExtensionBlockquote, ExtensionBlockquote,
ExtensionBold, ExtensionBold,
ExtensionBulletList, ExtensionBulletList,
ExtensionClearFormat,
ExtensionCode, ExtensionCode,
ExtensionCodeBlock,
ExtensionColor,
ExtensionColumn,
ExtensionColumns,
ExtensionCommands,
ExtensionDocument, ExtensionDocument,
ExtensionDraggable,
ExtensionDropcursor, ExtensionDropcursor,
ExtensionFontSize,
ExtensionFormatBrush,
ExtensionGapcursor, ExtensionGapcursor,
ExtensionHardBreak, ExtensionHardBreak,
ExtensionHeading, ExtensionHeading,
ExtensionHighlight,
ExtensionHistory, ExtensionHistory,
ExtensionHorizontalRule, ExtensionHorizontalRule,
ExtensionIframe,
ExtensionIndent,
ExtensionItalic, ExtensionItalic,
ExtensionOrderedList,
ExtensionStrike,
ExtensionText,
ExtensionTaskList,
ExtensionLink, ExtensionLink,
ExtensionTextAlign, ExtensionListKeymap,
ExtensionUnderline, ExtensionNodeSelected,
ExtensionTable, ExtensionOrderedList,
ExtensionPlaceholder,
ExtensionSearchAndReplace,
ExtensionStrike,
ExtensionSubscript, ExtensionSubscript,
ExtensionSuperscript, ExtensionSuperscript,
ExtensionPlaceholder, ExtensionTable,
ExtensionHighlight, ExtensionTaskList,
ExtensionCommands, ExtensionText,
ExtensionIframe, ExtensionTextAlign,
ExtensionCodeBlock,
ExtensionFontSize,
ExtensionColor,
ExtensionIndent,
lowlight,
type AnyExtension,
Editor,
ToolboxItem,
ExtensionDraggable,
ExtensionColumns,
ExtensionColumn,
ExtensionNodeSelected,
ExtensionTrailingNode, ExtensionTrailingNode,
ToolbarItem, ExtensionUnderline,
Plugin, Plugin,
PluginKey, PluginKey,
DecorationSet, RichTextEditor,
ExtensionListKeymap, ToolbarItem,
ExtensionSearchAndReplace, ToolboxItem,
ExtensionClearFormat, lowlight,
ExtensionFormatBrush, type AnyExtension,
} from "@halo-dev/richtext-editor"; } from "@halo-dev/richtext-editor";
// ui custom extension // ui custom extension
import { import { i18n } from "@/locales";
UiExtensionAudio, import { usePluginModuleStore } from "@/stores/plugin";
UiExtensionImage, import { formatDatetime } from "@/utils/date";
UiExtensionUpload, import { usePermission } from "@/utils/permission";
UiExtensionVideo, import AttachmentSelectorModal from "@console/modules/contents/attachments/components/AttachmentSelectorModal.vue";
} from "./extensions"; import type { Attachment } from "@halo-dev/api-client";
import { import {
IconCalendar, IconCalendar,
IconCharacterRecognition, IconCharacterRecognition,
@ -66,8 +66,23 @@ import {
VTabItem, VTabItem,
VTabs, VTabs,
} from "@halo-dev/components"; } from "@halo-dev/components";
import AttachmentSelectorModal from "@console/modules/contents/attachments/components/AttachmentSelectorModal.vue"; import type { AttachmentLike } from "@halo-dev/console-shared";
import ExtensionCharacterCount from "@tiptap/extension-character-count"; import ExtensionCharacterCount from "@tiptap/extension-character-count";
import { useDebounceFn, useLocalStorage } from "@vueuse/core";
import type { AxiosRequestConfig } from "axios";
import { OverlayScrollbarsComponent } from "overlayscrollbars-vue";
import {
inject,
markRaw,
nextTick,
onBeforeUnmount,
onMounted,
ref,
shallowRef,
watch,
type ComputedRef,
} from "vue";
import { useI18n } from "vue-i18n";
import MdiFormatHeader1 from "~icons/mdi/format-header-1"; import MdiFormatHeader1 from "~icons/mdi/format-header-1";
import MdiFormatHeader2 from "~icons/mdi/format-header-2"; import MdiFormatHeader2 from "~icons/mdi/format-header-2";
import MdiFormatHeader3 from "~icons/mdi/format-header-3"; import MdiFormatHeader3 from "~icons/mdi/format-header-3";
@ -75,29 +90,14 @@ import MdiFormatHeader4 from "~icons/mdi/format-header-4";
import MdiFormatHeader5 from "~icons/mdi/format-header-5"; import MdiFormatHeader5 from "~icons/mdi/format-header-5";
import MdiFormatHeader6 from "~icons/mdi/format-header-6"; import MdiFormatHeader6 from "~icons/mdi/format-header-6";
import RiLayoutRightLine from "~icons/ri/layout-right-line"; import RiLayoutRightLine from "~icons/ri/layout-right-line";
import {
inject,
markRaw,
ref,
watch,
onMounted,
shallowRef,
type ComputedRef,
} from "vue";
import { formatDatetime } from "@/utils/date";
import { useAttachmentSelect } from "./composables/use-attachment"; import { useAttachmentSelect } from "./composables/use-attachment";
import type { Attachment } from "@halo-dev/api-client"; import {
import { useI18n } from "vue-i18n"; UiExtensionAudio,
import { i18n } from "@/locales"; UiExtensionImage,
import { OverlayScrollbarsComponent } from "overlayscrollbars-vue"; UiExtensionUpload,
import { usePluginModuleStore } from "@/stores/plugin"; UiExtensionVideo,
import type { AttachmentLike, PluginModule } from "@halo-dev/console-shared"; } from "./extensions";
import { useDebounceFn, useLocalStorage } from "@vueuse/core";
import { onBeforeUnmount } from "vue";
import { usePermission } from "@/utils/permission";
import type { AxiosRequestConfig } from "axios";
import { getContents } from "./utils/attachment"; import { getContents } from "./utils/attachment";
import { nextTick } from "vue";
const { t } = useI18n(); const { t } = useI18n();
const { currentUserHasPermission } = usePermission(); const { currentUserHasPermission } = usePermission();
@ -189,20 +189,21 @@ const handleCloseAttachmentSelectorModal = () => {
attachmentOptions.value = initAttachmentOptions; attachmentOptions.value = initAttachmentOptions;
}; };
onMounted(() => { onMounted(async () => {
const extensionsFromPlugins: AnyExtension[] = []; const extensionsFromPlugins: AnyExtension[] = [];
pluginModules.forEach((pluginModule: PluginModule) => {
const { extensionPoints } = pluginModule; for (const pluginModule of pluginModules) {
if (!extensionPoints?.["default:editor:extension:create"]) { const callbackFunction =
return; pluginModule?.extensionPoints?.["default:editor:extension:create"];
if (typeof callbackFunction !== "function") {
continue;
} }
const extensions = extensionPoints[ const extensions = await callbackFunction();
"default:editor:extension:create"
]() as [];
extensionsFromPlugins.push(...extensions); extensionsFromPlugins.push(...extensions);
}); }
// debounce OnUpdate // debounce OnUpdate
const debounceOnUpdate = useDebounceFn(() => { const debounceOnUpdate = useDebounceFn(() => {

View File

@ -1,8 +1,8 @@
import { usePluginModuleStore } from "@/stores/plugin";
import type { EditorProvider, PluginModule } from "@halo-dev/console-shared";
import { onMounted, ref, type Ref, defineAsyncComponent, markRaw } from "vue";
import { VLoading } from "@halo-dev/components";
import Logo from "@/assets/logo.png"; import Logo from "@/assets/logo.png";
import { usePluginModuleStore } from "@/stores/plugin";
import { VLoading } from "@halo-dev/components";
import type { EditorProvider } from "@halo-dev/console-shared";
import { defineAsyncComponent, markRaw, onMounted, ref, type Ref } from "vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
interface useEditorExtensionPointsReturn { interface useEditorExtensionPointsReturn {
@ -30,17 +30,23 @@ export function useEditorExtensionPoints(): useEditorExtensionPointsReturn {
}, },
]); ]);
onMounted(() => { onMounted(async () => {
pluginModules.forEach((pluginModule: PluginModule) => { for (const pluginModule of pluginModules) {
const { extensionPoints } = pluginModule; try {
if (!extensionPoints?.["editor:create"]) { const callbackFunction =
return; pluginModule?.extensionPoints?.["editor:create"];
if (typeof callbackFunction !== "function") {
continue;
}
const providers = await callbackFunction();
editorProviders.value.push(...providers);
} catch (error) {
console.error(`Error processing plugin module:`, pluginModule, error);
} }
}
const providers = extensionPoints["editor:create"]() as EditorProvider[];
editorProviders.value.push(...providers);
});
}); });
return { return {

View File

@ -1,33 +1,33 @@
<script lang="ts" setup> <script lang="ts" setup>
import UserAvatar from "@/components/user-avatar/UserAvatar.vue";
import { usePluginModuleStore } from "@/stores/plugin";
import { apiClient } from "@/utils/api-client"; import { apiClient } from "@/utils/api-client";
import type { DetailedUser } from "@halo-dev/api-client";
import { import {
VButton, VButton,
VDropdown, VDropdown,
VDropdownItem, VDropdownItem,
VTabbar, VTabbar,
} from "@halo-dev/components"; } from "@halo-dev/components";
import type { UserProfileTab } from "@halo-dev/console-shared";
import { useQuery } from "@tanstack/vue-query";
import { useRouteQuery } from "@vueuse/router";
import { import {
computed, computed,
markRaw, markRaw,
onMounted, onMounted,
provide, provide,
type Ref,
ref, ref,
toRaw, toRaw,
type Ref,
} from "vue"; } from "vue";
import type { DetailedUser } from "@halo-dev/api-client";
import ProfileEditingModal from "./components/ProfileEditingModal.vue";
import PasswordChangeModal from "./components/PasswordChangeModal.vue";
import { useQuery } from "@tanstack/vue-query";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import UserAvatar from "@/components/user-avatar/UserAvatar.vue"; import PasswordChangeModal from "./components/PasswordChangeModal.vue";
import ProfileEditingModal from "./components/ProfileEditingModal.vue";
import DetailTab from "./tabs/Detail.vue"; import DetailTab from "./tabs/Detail.vue";
import PersonalAccessTokensTab from "./tabs/PersonalAccessTokens.vue";
import { useRouteQuery } from "@vueuse/router";
import NotificationPreferences from "./tabs/NotificationPreferences.vue"; import NotificationPreferences from "./tabs/NotificationPreferences.vue";
import PersonalAccessTokensTab from "./tabs/PersonalAccessTokens.vue";
import TwoFactor from "./tabs/TwoFactor.vue"; import TwoFactor from "./tabs/TwoFactor.vue";
import type { PluginModule, UserProfileTab } from "@halo-dev/console-shared";
import { usePluginModuleStore } from "@/stores/plugin";
const { t } = useI18n(); const { t } = useI18n();
@ -76,21 +76,24 @@ const tabs = ref<UserProfileTab[]>([
]); ]);
// Collect uc:profile:tabs:create extension points // Collect uc:profile:tabs:create extension points
onMounted(() => { const { pluginModules } = usePluginModuleStore();
const { pluginModules } = usePluginModuleStore();
pluginModules.forEach((pluginModule: PluginModule) => { onMounted(async () => {
const { extensionPoints } = pluginModule; for (const pluginModule of pluginModules) {
if (!extensionPoints?.["uc:user:profile:tabs:create"]) { try {
return; const callbackFunction =
pluginModule?.extensionPoints?.["uc:user:profile:tabs:create"];
if (typeof callbackFunction !== "function") {
continue;
}
const providers = await callbackFunction();
tabs.value.push(...providers);
} catch (error) {
console.error(`Error processing plugin module:`, pluginModule, error);
} }
}
const providers = extensionPoints[
"uc:user:profile:tabs:create"
]() as UserProfileTab[];
tabs.value.push(...providers);
});
}); });
const tabbarItems = computed(() => { const tabbarItems = computed(() => {