feat: make plugin tabs extensible (#4041)

#### What type of PR is this?

/area console
/kind feature
/milestone 2.7.x

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

插件自身的详情页面中的 Tabs 选项卡支持拓展,允许开发者自行为插件编写设置界面、一些不常用的操作页面等。

示例:

<img width="1358" alt="image" src="https://github.com/halo-dev/halo/assets/21301288/1e414e4e-688d-47de-907f-5f2a0afa9350">

扩展方式:

参考文档:https://github.com/ruibaby/halo/blob/feat/plugin-tabs-extend/console/docs/extension-points/plugin-self-tabs.md

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

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

#### Special notes for your reviewer:

需要测试:

1. 测试任意插件的详情页面是否能够正常使用。
2. 测试任意插件的设置表单能否正常使用。
3. 可以尝试根据文档为某个插件添加自定义的选项卡,测试是否能够正常工作。

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

```release-note
Console 端插件详情选项卡支持通过插件扩展。
```
pull/4195/head
Ryan Wang 2023-07-07 12:38:11 +08:00 committed by GitHub
parent bf2b92e77c
commit c0aae3a63c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 393 additions and 397 deletions

View File

@ -0,0 +1,40 @@
# 插件详情选项卡扩展点
## 原由
部分插件可能需要在 Console 端自行实现 UI 以完成一些自定义的需求,但可能并不希望在菜单中添加一个菜单项,所以希望可以在插件详情页面添加一个自定义 UI 的选项卡。
## 定义方式
```ts
import { definePlugin, PluginTab } from "@halo-dev/console-shared";
import MyComponent from "@/views/my-component.vue";
import { markRaw } from "vue";
export default definePlugin({
components: {},
routes: [],
extensionPoints: {
"plugin:self:tabs:create": () : PluginTab[] => {
return [
{
id: "my-tab-panel",
label: "Custom Panel",
component: markRaw(MyComponent),
permissions: []
},
];
},
},
});
```
PluginTab 类型:
```ts
export interface PluginTab {
id: string;
label: string;
component: Raw<Component>;
permissions?: string[];
}
```

View File

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

View File

@ -0,0 +1,8 @@
import type { Component, Raw } from "vue";
export interface PluginTab {
id: string;
label: string;
component: Raw<Component>;
permissions?: string[];
}

View File

@ -2,7 +2,7 @@ import type { Component } from "vue";
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 { EditorProvider, PluginTab } from "..";
import type { AnyExtension } from "@tiptap/vue-3";
import type { CommentSubjectRefProvider } from "@/states/comment-subject-ref";
@ -21,6 +21,8 @@ export interface ExtensionPoint {
"editor:create"?: () => EditorProvider[] | Promise<EditorProvider[]>;
"plugin:self:tabs:create"?: () => PluginTab[] | Promise<PluginTab[]>;
"default:editor:extension:create": () =>
| AnyExtension[]
| Promise<AnyExtension[]>;

View File

@ -788,8 +788,6 @@ core:
license: License
role_templates: Role Templates
last_starttime: Last Start Time
settings:
title: Plugin settings
loader:
toast:
entry_load_failed: "{name}: Failed to load plugin entry file"

View File

@ -788,8 +788,6 @@ core:
license: 协议
role_templates: 权限模板
last_starttime: 最近一次启动
settings:
title: 插件设置
loader:
toast:
entry_load_failed: "{name}:加载插件入口文件失败"

View File

@ -788,8 +788,6 @@ core:
license: 協議
role_templates: 權限模板
last_starttime: 最近一次啟動
settings:
title: 插件設置
loader:
toast:
entry_load_failed: "{name}:讀取插件入口文件失敗"

View File

@ -1,204 +1,148 @@
<script lang="ts" setup>
import {
VAlert,
VDescription,
VDescriptionItem,
VSwitch,
} from "@halo-dev/components";
import type { Ref } from "vue";
import { computed, inject } from "vue";
// core libs
import { provide, ref, computed, onMounted } from "vue";
import { useRoute } from "vue-router";
import { apiClient } from "@/utils/api-client";
import type { Plugin, Role } from "@halo-dev/api-client";
import { pluginLabels, roleLabels } from "@/constants/labels";
import { rbacAnnotations } from "@/constants/annotations";
import { usePluginLifeCycle } from "./composables/use-plugin";
import { formatDatetime } from "@/utils/date";
// libs
import cloneDeep from "lodash.clonedeep";
// components
import { VCard, VPageHeader, VTabbar, VAvatar } from "@halo-dev/components";
// types
import type { Ref } from "vue";
import type { Plugin, Setting, SettingForm } from "@halo-dev/api-client";
import { usePermission } from "@/utils/permission";
import { useI18n } from "vue-i18n";
import { useQuery } from "@tanstack/vue-query";
import type { PluginTab } from "@halo-dev/console-shared";
import { usePluginModuleStore } from "@/stores/plugin";
import { markRaw } from "vue";
import DetailTab from "./tabs/Detail.vue";
import SettingTab from "./tabs/Setting.vue";
import { useRouteQuery } from "@vueuse/router";
const plugin = inject<Ref<Plugin | undefined>>("plugin");
const { changeStatus } = usePluginLifeCycle(plugin);
const { currentUserHasPermission } = usePermission();
const { t } = useI18n();
interface RoleTemplateGroup {
module: string | null | undefined;
roles: Role[];
}
const { data: pluginRoleTemplates } = useQuery({
queryKey: ["plugin-roles", plugin?.value?.metadata.name],
queryFn: async () => {
const { data } = await apiClient.extension.role.listv1alpha1Role({
page: 0,
size: 0,
labelSelector: [
`${pluginLabels.NAME}=${plugin?.value?.metadata.name}`,
`${roleLabels.TEMPLATE}=true`,
"!halo.run/hidden",
],
});
return data.items;
const initialTabs = ref<PluginTab[]>([
{
id: "detail",
label: t("core.plugin.tabs.detail"),
component: markRaw(DetailTab),
},
cacheTime: 0,
enabled: computed(() => !!plugin?.value?.metadata.name),
]);
const route = useRoute();
const tabs = ref<PluginTab[]>(cloneDeep(initialTabs.value));
const activeTab = useRouteQuery<string>("tab", tabs.value[0].id, {
mode: "push",
});
const pluginRoleTemplateGroups = computed<RoleTemplateGroup[]>(() => {
const groups: RoleTemplateGroup[] = [];
pluginRoleTemplates.value?.forEach((role) => {
const group = groups.find(
(group) =>
group.module === role.metadata.annotations?.[rbacAnnotations.MODULE]
);
if (group) {
group.roles.push(role);
} else {
groups.push({
module: role.metadata.annotations?.[rbacAnnotations.MODULE],
roles: [role],
provide<Ref<string | undefined>>("activeTab", activeTab);
const { data: plugin } = useQuery({
queryKey: ["plugin", route.params.name],
queryFn: async () => {
const { data } =
await apiClient.extension.plugin.getpluginHaloRunV1alpha1Plugin({
name: route.params.name as string,
});
return data;
},
});
provide<Ref<Plugin | undefined>>("plugin", plugin);
const { data: setting } = useQuery({
queryKey: ["plugin-setting", plugin],
queryFn: async () => {
const { data } = await apiClient.plugin.fetchPluginSetting({
name: plugin.value?.metadata.name as string,
});
return data;
},
enabled: computed(() => {
return (
!!plugin.value &&
!!plugin.value.spec.settingName &&
currentUserHasPermission(["system:plugins:manage"])
);
}),
async onSuccess(data) {
if (data) {
const { forms } = data.spec;
tabs.value = [
...initialTabs.value,
...forms.map((item: SettingForm) => {
return {
id: item.group,
label: item.label || "",
component: markRaw(SettingTab),
};
}),
] as PluginTab[];
}
},
});
provide<Ref<Setting | undefined>>("setting", setting);
onMounted(() => {
const { pluginModules } = usePluginModuleStore();
const currentPluginModule = pluginModules.find(
(item) => item.extension.metadata.name === route.params.name
);
if (!currentPluginModule) {
return;
}
const { extensionPoints } = currentPluginModule;
if (!extensionPoints?.["plugin:self:tabs:create"]) {
return;
}
const extraTabs = extensionPoints["plugin:self:tabs:create"]() as PluginTab[];
extraTabs.forEach((tab) => {
if (currentUserHasPermission(tab.permissions)) {
initialTabs.value.push(tab);
}
});
return groups;
});
</script>
<template>
<Transition mode="out-in" name="fade">
<div>
<div class="flex items-center justify-between bg-white px-4 py-4 sm:px-6">
<div>
<h3 class="text-lg font-medium leading-6 text-gray-900">
{{ $t("core.plugin.detail.header.title") }}
</h3>
</div>
<div v-permission="['system:plugins:manage']">
<VSwitch :model-value="plugin?.spec.enabled" @change="changeStatus" />
</div>
<VPageHeader :title="plugin?.spec?.displayName">
<template #icon>
<VAvatar
v-if="plugin"
:src="plugin.status?.logo"
:alt="plugin.spec.displayName"
class="mr-2"
size="sm"
/>
</template>
</VPageHeader>
<div class="m-0 md:m-4">
<VCard :body-class="['!p-0']">
<template #header>
<VTabbar
v-model:active-id="activeTab"
:items="tabs.map((item) => ({ id: item.id, label: item.label }))"
class="w-full !rounded-none"
type="outline"
></VTabbar>
</template>
<div class="bg-white">
<template v-for="tab in tabs" :key="tab.id">
<component :is="tab.component" v-if="activeTab === tab.id" />
</template>
</div>
<div
v-if="
plugin?.status?.phase === 'FAILED' &&
plugin?.status?.conditions?.length
"
class="w-full px-4 pb-2 sm:px-6"
>
<VAlert
type="error"
:title="plugin?.status?.conditions?.[0].reason"
:description="plugin?.status?.conditions?.[0].message"
:closable="false"
/>
</div>
<div class="border-t border-gray-200">
<VDescription>
<VDescriptionItem
:label="$t('core.plugin.detail.fields.display_name')"
:content="plugin?.spec.displayName"
/>
<VDescriptionItem
:label="$t('core.plugin.detail.fields.description')"
:content="plugin?.spec.description"
/>
<VDescriptionItem
:label="$t('core.plugin.detail.fields.version')"
:content="plugin?.spec.version"
/>
<VDescriptionItem
:label="$t('core.plugin.detail.fields.requires')"
:content="plugin?.spec.requires"
/>
<VDescriptionItem :label="$t('core.plugin.detail.fields.author')">
<a
v-if="plugin?.spec.author"
:href="plugin?.spec.author.website"
target="_blank"
>
{{ plugin?.spec.author.name }}
</a>
<span v-else>
{{ $t("core.common.text.none") }}
</span>
</VDescriptionItem>
<VDescriptionItem :label="$t('core.plugin.detail.fields.license')">
<ul
v-if="plugin?.spec.license && plugin?.spec.license.length"
class="list-inside"
:class="{ 'list-disc': plugin?.spec.license.length > 1 }"
>
<li v-for="(license, index) in plugin.spec.license" :key="index">
<a v-if="license.url" :href="license.url" target="_blank">
{{ license.name }}
</a>
<span v-else>
{{ license.name }}
</span>
</li>
</ul>
</VDescriptionItem>
<VDescriptionItem
:label="$t('core.plugin.detail.fields.role_templates')"
>
<dl
v-if="pluginRoleTemplateGroups.length"
class="divide-y divide-gray-100"
>
<div
v-for="(group, groupIndex) in pluginRoleTemplateGroups"
:key="groupIndex"
class="rounded bg-gray-50 px-4 py-5 hover:bg-gray-100 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6"
>
<dt class="text-sm font-medium text-gray-900">
{{ group.module }}
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
<ul class="space-y-2">
<li v-for="(role, index) in group.roles" :key="index">
<div
class="inline-flex w-72 cursor-pointer flex-row items-center gap-4 rounded border p-5 hover:border-primary"
>
<div class="inline-flex flex-col gap-y-3">
<span class="font-medium text-gray-900">
{{
role.metadata.annotations?.[
rbacAnnotations.DISPLAY_NAME
]
}}
</span>
<span
v-if="
role.metadata.annotations?.[
rbacAnnotations.DEPENDENCIES
]
"
class="text-xs text-gray-400"
>
{{
$t("core.role.common.text.dependent_on", {
roles: JSON.parse(
role.metadata.annotations?.[
rbacAnnotations.DEPENDENCIES
]
).join(", "),
})
}}
</span>
</div>
</div>
</li>
</ul>
</dd>
</div>
</dl>
<span v-else>
{{ $t("core.common.text.none") }}
</span>
</VDescriptionItem>
<VDescriptionItem
:label="$t('core.plugin.detail.fields.last_starttime')"
:content="formatDatetime(plugin?.status?.lastStartTime)"
/>
</VDescription>
</div>
</div>
</Transition>
</VCard>
</div>
</template>

View File

@ -1,184 +0,0 @@
<script lang="ts" setup>
// core libs
import { nextTick, provide, ref, computed, watch } from "vue";
import { RouterView, useRoute, useRouter } from "vue-router";
import { apiClient } from "@/utils/api-client";
// libs
import cloneDeep from "lodash.clonedeep";
// components
import {
VCard,
VPageHeader,
VTabbar,
VAvatar,
VLoading,
} from "@halo-dev/components";
import BasicLayout from "@/layouts/BasicLayout.vue";
// types
import type { Ref } from "vue";
import type { Plugin, Setting, SettingForm } from "@halo-dev/api-client";
import { usePermission } from "@/utils/permission";
import { useI18n } from "vue-i18n";
import { useQuery } from "@tanstack/vue-query";
const { currentUserHasPermission } = usePermission();
const { t } = useI18n();
interface PluginTab {
id: string;
label: string;
route: {
name: string;
params?: Record<string, string>;
};
}
const initialTabs: PluginTab[] = [
{
id: "detail",
label: t("core.plugin.tabs.detail"),
route: {
name: "PluginDetail",
},
},
];
const route = useRoute();
const router = useRouter();
const tabs = ref<PluginTab[]>(cloneDeep(initialTabs));
const activeTab = ref<string>(tabs.value[0].id);
provide<Ref<string | undefined>>("activeTab", activeTab);
const { data: plugin } = useQuery({
queryKey: ["plugin", route.params.name],
queryFn: async () => {
const { data } =
await apiClient.extension.plugin.getpluginHaloRunV1alpha1Plugin({
name: route.params.name as string,
});
return data;
},
});
provide<Ref<Plugin | undefined>>("plugin", plugin);
const { data: setting } = useQuery({
queryKey: ["plugin-setting", plugin],
queryFn: async () => {
const { data } = await apiClient.plugin.fetchPluginSetting({
name: plugin.value?.metadata.name as string,
});
return data;
},
enabled: computed(() => {
return (
!!plugin.value &&
!!plugin.value.spec.settingName &&
currentUserHasPermission(["system:plugins:manage"])
);
}),
async onSuccess(data) {
if (data) {
const { forms } = data.spec;
tabs.value = [
...initialTabs,
...forms.map((item: SettingForm) => {
return {
id: item.group,
label: item.label || "",
route: {
name: "PluginSetting",
params: {
group: item.group,
},
},
};
}),
] as PluginTab[];
}
await nextTick();
handleTriggerTabChange();
},
});
provide<Ref<Setting | undefined>>("setting", setting);
const handleTabChange = (id: string) => {
const tab = tabs.value.find((item) => item.id === id);
if (tab) {
activeTab.value = tab.id;
router.push(tab.route);
}
};
const handleTriggerTabChange = () => {
if (route.name === "PluginSetting") {
const tab = tabs.value.find((tab) => {
return (
tab.route.name === route.name &&
tab.route.params?.group === route.params.group
);
});
if (tab) {
activeTab.value = tab.id;
return;
}
handleTabChange(tabs.value[0].id);
return;
}
const tab = tabs.value.find((tab) => tab.route.name === route.name);
activeTab.value = tab ? tab.id : tabs.value[0].id;
};
watch([() => route.name, () => route.params], () => {
handleTriggerTabChange();
});
</script>
<template>
<BasicLayout>
<VPageHeader :title="plugin?.spec?.displayName">
<template #icon>
<VAvatar
v-if="plugin"
:src="plugin.status?.logo"
:alt="plugin.spec.displayName"
class="mr-2"
size="sm"
/>
</template>
</VPageHeader>
<div class="m-0 md:m-4">
<VCard :body-class="['!p-0']">
<template #header>
<VTabbar
v-model:active-id="activeTab"
:items="tabs.map((item) => ({ id: item.id, label: item.label }))"
class="w-full !rounded-none"
type="outline"
@change="handleTabChange"
></VTabbar>
</template>
<div class="bg-white">
<RouterView :key="activeTab" v-slot="{ Component }">
<template v-if="Component">
<Suspense>
<component :is="Component"></component>
<template #fallback>
<VLoading />
</template>
</Suspense>
</template>
</RouterView>
</div>
</VCard>
</div>
</BasicLayout>
</template>

View File

@ -1,9 +1,7 @@
import { definePlugin } from "@halo-dev/console-shared";
import BasicLayout from "@/layouts/BasicLayout.vue";
import BlankLayout from "@/layouts/BlankLayout.vue";
import PluginLayout from "./layouts/PluginLayout.vue";
import PluginList from "./PluginList.vue";
import PluginSetting from "./PluginSetting.vue";
import PluginDetail from "./PluginDetail.vue";
import { IconPlug } from "@halo-dev/components";
import { markRaw } from "vue";
@ -39,10 +37,10 @@ export default definePlugin({
},
{
path: ":name",
component: PluginLayout,
component: BasicLayout,
children: [
{
path: "detail",
path: "",
name: "PluginDetail",
component: PluginDetail,
meta: {
@ -50,15 +48,6 @@ export default definePlugin({
permissions: ["system:plugins:view"],
},
},
{
path: "settings/:group",
name: "PluginSetting",
component: PluginSetting,
meta: {
title: "core.plugin.settings.title",
permissions: ["system:plugins:manage"],
},
},
],
},
],

View File

@ -0,0 +1,204 @@
<script lang="ts" setup>
import {
VAlert,
VDescription,
VDescriptionItem,
VSwitch,
} from "@halo-dev/components";
import type { Ref } from "vue";
import { computed, inject } from "vue";
import { apiClient } from "@/utils/api-client";
import type { Plugin, Role } from "@halo-dev/api-client";
import { pluginLabels, roleLabels } from "@/constants/labels";
import { rbacAnnotations } from "@/constants/annotations";
import { usePluginLifeCycle } from "../composables/use-plugin";
import { formatDatetime } from "@/utils/date";
import { useQuery } from "@tanstack/vue-query";
const plugin = inject<Ref<Plugin | undefined>>("plugin");
const { changeStatus } = usePluginLifeCycle(plugin);
interface RoleTemplateGroup {
module: string | null | undefined;
roles: Role[];
}
const { data: pluginRoleTemplates } = useQuery({
queryKey: ["plugin-roles", plugin?.value?.metadata.name],
queryFn: async () => {
const { data } = await apiClient.extension.role.listv1alpha1Role({
page: 0,
size: 0,
labelSelector: [
`${pluginLabels.NAME}=${plugin?.value?.metadata.name}`,
`${roleLabels.TEMPLATE}=true`,
"!halo.run/hidden",
],
});
return data.items;
},
cacheTime: 0,
enabled: computed(() => !!plugin?.value?.metadata.name),
});
const pluginRoleTemplateGroups = computed<RoleTemplateGroup[]>(() => {
const groups: RoleTemplateGroup[] = [];
pluginRoleTemplates.value?.forEach((role) => {
const group = groups.find(
(group) =>
group.module === role.metadata.annotations?.[rbacAnnotations.MODULE]
);
if (group) {
group.roles.push(role);
} else {
groups.push({
module: role.metadata.annotations?.[rbacAnnotations.MODULE],
roles: [role],
});
}
});
return groups;
});
</script>
<template>
<Transition mode="out-in" name="fade">
<div>
<div class="flex items-center justify-between bg-white px-4 py-4 sm:px-6">
<div>
<h3 class="text-lg font-medium leading-6 text-gray-900">
{{ $t("core.plugin.detail.header.title") }}
</h3>
</div>
<div v-permission="['system:plugins:manage']">
<VSwitch :model-value="plugin?.spec.enabled" @change="changeStatus" />
</div>
</div>
<div
v-if="
plugin?.status?.phase === 'FAILED' &&
plugin?.status?.conditions?.length
"
class="w-full px-4 pb-2 sm:px-6"
>
<VAlert
type="error"
:title="plugin?.status?.conditions?.[0].reason"
:description="plugin?.status?.conditions?.[0].message"
:closable="false"
/>
</div>
<div class="border-t border-gray-200">
<VDescription>
<VDescriptionItem
:label="$t('core.plugin.detail.fields.display_name')"
:content="plugin?.spec.displayName"
/>
<VDescriptionItem
:label="$t('core.plugin.detail.fields.description')"
:content="plugin?.spec.description"
/>
<VDescriptionItem
:label="$t('core.plugin.detail.fields.version')"
:content="plugin?.spec.version"
/>
<VDescriptionItem
:label="$t('core.plugin.detail.fields.requires')"
:content="plugin?.spec.requires"
/>
<VDescriptionItem :label="$t('core.plugin.detail.fields.author')">
<a
v-if="plugin?.spec.author"
:href="plugin?.spec.author.website"
target="_blank"
>
{{ plugin?.spec.author.name }}
</a>
<span v-else>
{{ $t("core.common.text.none") }}
</span>
</VDescriptionItem>
<VDescriptionItem :label="$t('core.plugin.detail.fields.license')">
<ul
v-if="plugin?.spec.license && plugin?.spec.license.length"
class="list-inside"
:class="{ 'list-disc': plugin?.spec.license.length > 1 }"
>
<li v-for="(license, index) in plugin.spec.license" :key="index">
<a v-if="license.url" :href="license.url" target="_blank">
{{ license.name }}
</a>
<span v-else>
{{ license.name }}
</span>
</li>
</ul>
</VDescriptionItem>
<VDescriptionItem
:label="$t('core.plugin.detail.fields.role_templates')"
>
<dl
v-if="pluginRoleTemplateGroups.length"
class="divide-y divide-gray-100"
>
<div
v-for="(group, groupIndex) in pluginRoleTemplateGroups"
:key="groupIndex"
class="rounded bg-gray-50 px-4 py-5 hover:bg-gray-100 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6"
>
<dt class="text-sm font-medium text-gray-900">
{{ group.module }}
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
<ul class="space-y-2">
<li v-for="(role, index) in group.roles" :key="index">
<div
class="inline-flex w-72 cursor-pointer flex-row items-center gap-4 rounded border p-5 hover:border-primary"
>
<div class="inline-flex flex-col gap-y-3">
<span class="font-medium text-gray-900">
{{
role.metadata.annotations?.[
rbacAnnotations.DISPLAY_NAME
]
}}
</span>
<span
v-if="
role.metadata.annotations?.[
rbacAnnotations.DEPENDENCIES
]
"
class="text-xs text-gray-400"
>
{{
$t("core.role.common.text.dependent_on", {
roles: JSON.parse(
role.metadata.annotations?.[
rbacAnnotations.DEPENDENCIES
]
).join(", "),
})
}}
</span>
</div>
</div>
</li>
</ul>
</dd>
</div>
</dl>
<span v-else>
{{ $t("core.common.text.none") }}
</span>
</VDescriptionItem>
<VDescriptionItem
:label="$t('core.plugin.detail.fields.last_starttime')"
:content="formatDatetime(plugin?.status?.lastStartTime)"
/>
</VDescription>
</div>
</div>
</Transition>
</template>

View File

@ -11,7 +11,7 @@ import { Toast, VButton } from "@halo-dev/components";
// types
import type { ConfigMap, Plugin, Setting } from "@halo-dev/api-client";
import { useRouteParams } from "@vueuse/router";
import { useRouteQuery } from "@vueuse/router";
import { useI18n } from "vue-i18n";
import { useQuery, useQueryClient } from "@tanstack/vue-query";
import { toRaw } from "vue";
@ -19,13 +19,13 @@ import { toRaw } from "vue";
const { t } = useI18n();
const queryClient = useQueryClient();
const group = useRouteParams<string>("group");
const group = useRouteQuery<string>("tab", undefined, { mode: "push" });
const plugin = inject<Ref<Plugin | undefined>>("plugin");
const setting = inject<Ref<Setting | undefined>>("setting", ref());
const saving = ref(false);
const { data: configMap, suspense } = useQuery<ConfigMap>({
const { data: configMap } = useQuery<ConfigMap>({
queryKey: ["plugin-configMap", plugin],
queryFn: async () => {
const { data } = await apiClient.plugin.fetchPluginConfig({
@ -63,8 +63,6 @@ const handleSaveConfigMap = async () => {
saving.value = false;
};
await suspense();
</script>
<template>
<Transition mode="out-in" name="fade">