mirror of https://github.com/halo-dev/halo
feat: add plugin detail modal as global component (#6233)
#### What type of PR is this? /area ui /kind feature /milestone 2.17.x #### What this PR does / why we need it: 添加 PluginDetailModal,用于打开插件的设置界面。并在扩展设置页面适配以测试。 <img width="1643" alt="image" src="https://github.com/halo-dev/halo/assets/21301288/4bb38ab1-ed51-4437-8202-ccaf9f79cb41"> #### Which issue(s) this PR fixes: Fixes https://github.com/halo-dev/halo/issues/6232 #### Does this PR introduce a user-facing change? ```release-note 为 UI 添加通用的插件设置弹窗,以供插件主动调用 ```pull/6242/head
parent
8a61a39be3
commit
2aaf64aa34
|
@ -1,123 +1,23 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
// core libs
|
import type { Plugin, Setting } from "@halo-dev/api-client";
|
||||||
import { consoleApiClient, coreApiClient } from "@halo-dev/api-client";
|
|
||||||
import { computed, provide, ref } from "vue";
|
|
||||||
import { useRoute } from "vue-router";
|
|
||||||
|
|
||||||
// libs
|
|
||||||
import { cloneDeep } from "lodash-es";
|
|
||||||
|
|
||||||
// components
|
|
||||||
import { VAvatar, VCard, VPageHeader, VTabbar } from "@halo-dev/components";
|
import { VAvatar, VCard, VPageHeader, VTabbar } from "@halo-dev/components";
|
||||||
|
|
||||||
// types
|
|
||||||
import { usePluginModuleStore } from "@/stores/plugin";
|
|
||||||
import { usePermission } from "@/utils/permission";
|
|
||||||
import type { Plugin, Setting, SettingForm } from "@halo-dev/api-client";
|
|
||||||
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 type { Ref } from "vue";
|
||||||
import { markRaw } from "vue";
|
import { provide, toRefs } from "vue";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useRoute } from "vue-router";
|
||||||
import DetailTab from "./tabs/Detail.vue";
|
import { usePluginDetailTabs } from "./composables/use-plugin";
|
||||||
import SettingTab from "./tabs/Setting.vue";
|
|
||||||
|
|
||||||
const { currentUserHasPermission } = usePermission();
|
|
||||||
const { t } = useI18n();
|
|
||||||
|
|
||||||
const initialTabs = ref<PluginTab[]>([
|
|
||||||
{
|
|
||||||
id: "detail",
|
|
||||||
label: t("core.plugin.tabs.detail"),
|
|
||||||
component: markRaw(DetailTab),
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
||||||
const tabs = ref<PluginTab[]>(cloneDeep(initialTabs.value));
|
const { name } = toRefs(route.params);
|
||||||
const activeTab = useRouteQuery<string>("tab", tabs.value[0].id);
|
|
||||||
|
const { plugin, setting, activeTab, tabs } = usePluginDetailTabs(
|
||||||
|
name as Ref<string | undefined>,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
provide<Ref<string>>("activeTab", activeTab);
|
provide<Ref<string>>("activeTab", activeTab);
|
||||||
|
|
||||||
const { data: plugin } = useQuery({
|
|
||||||
queryKey: ["plugin", route.params.name],
|
|
||||||
queryFn: async () => {
|
|
||||||
const { data } = await coreApiClient.plugin.plugin.getPlugin({
|
|
||||||
name: route.params.name as string,
|
|
||||||
});
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
async onSuccess(data) {
|
|
||||||
if (
|
|
||||||
!data.spec.settingName ||
|
|
||||||
!currentUserHasPermission(["system:plugins:manage"])
|
|
||||||
) {
|
|
||||||
tabs.value = [...initialTabs.value, ...(await getTabsFromExtensions())];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
provide<Ref<Plugin | undefined>>("plugin", plugin);
|
provide<Ref<Plugin | undefined>>("plugin", plugin);
|
||||||
|
|
||||||
const { data: setting } = useQuery({
|
|
||||||
queryKey: ["plugin-setting", plugin],
|
|
||||||
queryFn: async () => {
|
|
||||||
const { data } = await consoleApiClient.plugin.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,
|
|
||||||
...(await getTabsFromExtensions()),
|
|
||||||
...forms.map((item: SettingForm) => {
|
|
||||||
return {
|
|
||||||
id: item.group,
|
|
||||||
label: item.label || "",
|
|
||||||
component: markRaw(SettingTab),
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
] as PluginTab[];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
provide<Ref<Setting | undefined>>("setting", setting);
|
provide<Ref<Setting | undefined>>("setting", setting);
|
||||||
|
|
||||||
async function getTabsFromExtensions() {
|
|
||||||
const { pluginModuleMap } = usePluginModuleStore();
|
|
||||||
|
|
||||||
const currentPluginModule = pluginModuleMap[route.params.name as string];
|
|
||||||
|
|
||||||
if (!currentPluginModule) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const callbackFunction =
|
|
||||||
currentPluginModule?.extensionPoints?.["plugin:self:tabs:create"];
|
|
||||||
|
|
||||||
if (typeof callbackFunction !== "function") {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const pluginTabs = await callbackFunction();
|
|
||||||
|
|
||||||
return pluginTabs.filter((tab) => {
|
|
||||||
return currentUserHasPermission(tab.permissions);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<VPageHeader :title="plugin?.spec?.displayName">
|
<VPageHeader :title="plugin?.spec?.displayName">
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { Plugin, Setting } from "@halo-dev/api-client";
|
||||||
|
import { IconLink, VButton, VModal, VTabbar } from "@halo-dev/components";
|
||||||
|
import { provide, ref, toRefs, type Ref } from "vue";
|
||||||
|
import { usePluginDetailTabs } from "../composables/use-plugin";
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{ name: string }>(), {});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: "close"): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const modal = ref<InstanceType<typeof VModal> | null>(null);
|
||||||
|
|
||||||
|
const { name } = toRefs(props);
|
||||||
|
|
||||||
|
const { plugin, setting, tabs, activeTab } = usePluginDetailTabs(name, false);
|
||||||
|
|
||||||
|
provide<Ref<string>>("activeTab", activeTab);
|
||||||
|
provide<Ref<Plugin | undefined>>("plugin", plugin);
|
||||||
|
provide<Ref<Setting | undefined>>("setting", setting);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<VModal
|
||||||
|
ref="modal"
|
||||||
|
:title="plugin?.spec.displayName"
|
||||||
|
:centered="true"
|
||||||
|
:width="920"
|
||||||
|
height="calc(100vh - 20px)"
|
||||||
|
mount-to-body
|
||||||
|
@close="emit('close')"
|
||||||
|
>
|
||||||
|
<template #actions>
|
||||||
|
<span>
|
||||||
|
<RouterLink
|
||||||
|
:to="{
|
||||||
|
name: 'PluginDetail',
|
||||||
|
params: { name },
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<IconLink />
|
||||||
|
</RouterLink>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<VTabbar
|
||||||
|
v-model:active-id="activeTab"
|
||||||
|
:items="
|
||||||
|
tabs.map((tab) => {
|
||||||
|
return { label: tab.label, id: tab.id };
|
||||||
|
})
|
||||||
|
"
|
||||||
|
type="outline"
|
||||||
|
/>
|
||||||
|
<div class="-m-4 mt-2">
|
||||||
|
<template v-for="tab in tabs" :key="tab.id">
|
||||||
|
<component :is="tab.component" v-if="activeTab === tab.id" />
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<VButton @click="modal?.close()">
|
||||||
|
{{ $t("core.common.buttons.close") }}
|
||||||
|
</VButton>
|
||||||
|
</template>
|
||||||
|
</VModal>
|
||||||
|
</template>
|
|
@ -11,7 +11,8 @@ import {
|
||||||
VEntityField,
|
VEntityField,
|
||||||
} from "@halo-dev/components";
|
} from "@halo-dev/components";
|
||||||
import { useQuery } from "@tanstack/vue-query";
|
import { useQuery } from "@tanstack/vue-query";
|
||||||
import { computed } from "vue";
|
import { computed, ref } from "vue";
|
||||||
|
import PluginDetailModal from "../PluginDetailModal.vue";
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{ extensionDefinition: ExtensionDefinition }>(),
|
defineProps<{ extensionDefinition: ExtensionDefinition }>(),
|
||||||
|
@ -36,9 +37,16 @@ const matchedPlugin = computed(() => {
|
||||||
props.extensionDefinition.metadata.labels?.["plugin.halo.run/plugin-name"]
|
props.extensionDefinition.metadata.labels?.["plugin.halo.run/plugin-name"]
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const pluginDetailModalVisible = ref(false);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<PluginDetailModal
|
||||||
|
v-if="pluginDetailModalVisible && matchedPlugin"
|
||||||
|
:name="matchedPlugin.metadata.name"
|
||||||
|
@close="pluginDetailModalVisible = false"
|
||||||
|
/>
|
||||||
<VEntity>
|
<VEntity>
|
||||||
<template v-if="$slots['selection-indicator']" #checkbox>
|
<template v-if="$slots['selection-indicator']" #checkbox>
|
||||||
<slot name="selection-indicator" />
|
<slot name="selection-indicator" />
|
||||||
|
@ -61,15 +69,12 @@ const matchedPlugin = computed(() => {
|
||||||
<template v-if="matchedPlugin" #end>
|
<template v-if="matchedPlugin" #end>
|
||||||
<VEntityField>
|
<VEntityField>
|
||||||
<template #description>
|
<template #description>
|
||||||
<RouterLink
|
<div
|
||||||
class="cursor-pointer rounded p-1 text-gray-600 transition-all hover:text-blue-600 group-hover:bg-gray-200/60"
|
class="cursor-pointer rounded p-1 text-gray-600 transition-all hover:text-blue-600 group-hover:bg-gray-200/60"
|
||||||
:to="{
|
@click.prevent="pluginDetailModalVisible = true"
|
||||||
name: 'PluginDetail',
|
|
||||||
params: { name: matchedPlugin?.metadata.name },
|
|
||||||
}"
|
|
||||||
>
|
>
|
||||||
<IconSettings />
|
<IconSettings />
|
||||||
</RouterLink>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</VEntityField>
|
</VEntityField>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -131,6 +131,7 @@ async function onExtensionChange(e: Event) {
|
||||||
<label
|
<label
|
||||||
class="cursor-pointer transition-all"
|
class="cursor-pointer transition-all"
|
||||||
:class="{ 'pointer-events-none opacity-50': isSubmitting }"
|
:class="{ 'pointer-events-none opacity-50': isSubmitting }"
|
||||||
|
@click.stop
|
||||||
>
|
>
|
||||||
<ExtensionDefinitionListItem :extension-definition="item">
|
<ExtensionDefinitionListItem :extension-definition="item">
|
||||||
<template #selection-indicator>
|
<template #selection-indicator>
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import { rbacAnnotations } from "@/constants/annotations";
|
import { rbacAnnotations } from "@/constants/annotations";
|
||||||
import { pluginLabels, roleLabels } from "@/constants/labels";
|
import { pluginLabels, roleLabels } from "@/constants/labels";
|
||||||
import { formatDatetime } from "@/utils/date";
|
import { formatDatetime } from "@/utils/date";
|
||||||
|
import { usePermission } from "@/utils/permission";
|
||||||
import {
|
import {
|
||||||
PluginStatusPhaseEnum,
|
PluginStatusPhaseEnum,
|
||||||
coreApiClient,
|
coreApiClient,
|
||||||
|
@ -18,8 +19,10 @@ import {
|
||||||
import { useQuery } from "@tanstack/vue-query";
|
import { useQuery } from "@tanstack/vue-query";
|
||||||
import type { Ref } from "vue";
|
import type { Ref } from "vue";
|
||||||
import { computed, inject, ref } from "vue";
|
import { computed, inject, ref } from "vue";
|
||||||
import PluginConditionsModal from "../components/PluginConditionsModal.vue";
|
import { usePluginLifeCycle } from "../../composables/use-plugin";
|
||||||
import { usePluginLifeCycle } from "../composables/use-plugin";
|
import PluginConditionsModal from "../PluginConditionsModal.vue";
|
||||||
|
|
||||||
|
const { currentUserHasPermission } = usePermission();
|
||||||
|
|
||||||
const plugin = inject<Ref<Plugin | undefined>>("plugin");
|
const plugin = inject<Ref<Plugin | undefined>>("plugin");
|
||||||
const { changeStatus, changingStatus } = usePluginLifeCycle(plugin);
|
const { changeStatus, changingStatus } = usePluginLifeCycle(plugin);
|
||||||
|
@ -45,7 +48,11 @@ const { data: pluginRoleTemplates } = useQuery({
|
||||||
return data.items;
|
return data.items;
|
||||||
},
|
},
|
||||||
cacheTime: 0,
|
cacheTime: 0,
|
||||||
enabled: computed(() => !!plugin?.value?.metadata.name),
|
enabled: computed(
|
||||||
|
() =>
|
||||||
|
!!plugin?.value?.metadata.name &&
|
||||||
|
currentUserHasPermission(["system:roles:view"])
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
const pluginRoleTemplateGroups = computed<RoleTemplateGroup[]>(() => {
|
const pluginRoleTemplateGroups = computed<RoleTemplateGroup[]>(() => {
|
|
@ -1,19 +1,11 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
// core libs
|
|
||||||
import { computed, inject, ref, type Ref } from "vue";
|
|
||||||
|
|
||||||
// hooks
|
|
||||||
import { useSettingFormConvert } from "@console/composables/use-setting-form";
|
|
||||||
import { consoleApiClient } from "@halo-dev/api-client";
|
|
||||||
|
|
||||||
// components
|
|
||||||
import { Toast, VButton } from "@halo-dev/components";
|
|
||||||
|
|
||||||
// types
|
|
||||||
import StickyBlock from "@/components/sticky-block/StickyBlock.vue";
|
import StickyBlock from "@/components/sticky-block/StickyBlock.vue";
|
||||||
|
import { useSettingFormConvert } from "@console/composables/use-setting-form";
|
||||||
import type { ConfigMap, Plugin, Setting } from "@halo-dev/api-client";
|
import type { ConfigMap, Plugin, Setting } from "@halo-dev/api-client";
|
||||||
|
import { consoleApiClient } from "@halo-dev/api-client";
|
||||||
|
import { Toast, VButton } from "@halo-dev/components";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/vue-query";
|
import { useQuery, useQueryClient } from "@tanstack/vue-query";
|
||||||
import { toRaw } from "vue";
|
import { computed, inject, ref, toRaw, type Ref } from "vue";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
|
@ -1,15 +1,21 @@
|
||||||
|
import { usePluginModuleStore } from "@/stores/plugin";
|
||||||
|
import { usePermission } from "@/utils/permission";
|
||||||
import {
|
import {
|
||||||
PluginStatusPhaseEnum,
|
PluginStatusPhaseEnum,
|
||||||
consoleApiClient,
|
consoleApiClient,
|
||||||
coreApiClient,
|
coreApiClient,
|
||||||
type Plugin,
|
type Plugin,
|
||||||
|
type SettingForm,
|
||||||
} from "@halo-dev/api-client";
|
} from "@halo-dev/api-client";
|
||||||
import type { ComputedRef, Ref } from "vue";
|
|
||||||
import { computed } from "vue";
|
|
||||||
|
|
||||||
import { Dialog, Toast } from "@halo-dev/components";
|
import { Dialog, Toast } from "@halo-dev/components";
|
||||||
import { useMutation } from "@tanstack/vue-query";
|
import type { PluginTab } from "@halo-dev/console-shared";
|
||||||
|
import { useMutation, useQuery } from "@tanstack/vue-query";
|
||||||
|
import { useRouteQuery } from "@vueuse/router";
|
||||||
|
import type { ComputedRef, Ref } from "vue";
|
||||||
|
import { computed, markRaw, ref } from "vue";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
|
import DetailTab from "../components/tabs/Detail.vue";
|
||||||
|
import SettingTab from "../components/tabs/Setting.vue";
|
||||||
|
|
||||||
interface usePluginLifeCycleReturn {
|
interface usePluginLifeCycleReturn {
|
||||||
isStarted: ComputedRef<boolean | undefined>;
|
isStarted: ComputedRef<boolean | undefined>;
|
||||||
|
@ -270,3 +276,105 @@ export function usePluginBatchOperations(names: Ref<string[]>) {
|
||||||
|
|
||||||
return { handleUninstallInBatch, handleChangeStatusInBatch };
|
return { handleUninstallInBatch, handleChangeStatusInBatch };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function usePluginDetailTabs(
|
||||||
|
pluginName: Ref<string | undefined>,
|
||||||
|
recordsActiveTab: boolean
|
||||||
|
) {
|
||||||
|
const { currentUserHasPermission } = usePermission();
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const initialTabs = [
|
||||||
|
{
|
||||||
|
id: "detail",
|
||||||
|
label: t("core.plugin.tabs.detail"),
|
||||||
|
component: markRaw(DetailTab),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const tabs = ref<PluginTab[]>(initialTabs);
|
||||||
|
const activeTab = recordsActiveTab
|
||||||
|
? useRouteQuery<string>("tab", tabs.value[0].id)
|
||||||
|
: ref(tabs.value[0].id);
|
||||||
|
|
||||||
|
const { data: plugin } = useQuery({
|
||||||
|
queryKey: ["plugin", pluginName],
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await coreApiClient.plugin.plugin.getPlugin({
|
||||||
|
name: pluginName.value as string,
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
async onSuccess(data) {
|
||||||
|
if (
|
||||||
|
!data.spec.settingName ||
|
||||||
|
!currentUserHasPermission(["system:plugins:manage"])
|
||||||
|
) {
|
||||||
|
tabs.value = [...initialTabs, ...(await getTabsFromExtensions())];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: setting } = useQuery({
|
||||||
|
queryKey: ["plugin-setting", plugin],
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await consoleApiClient.plugin.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,
|
||||||
|
...(await getTabsFromExtensions()),
|
||||||
|
...forms.map((item: SettingForm) => {
|
||||||
|
return {
|
||||||
|
id: item.group,
|
||||||
|
label: item.label || "",
|
||||||
|
component: markRaw(SettingTab),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
] as PluginTab[];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
async function getTabsFromExtensions() {
|
||||||
|
const { pluginModuleMap } = usePluginModuleStore();
|
||||||
|
|
||||||
|
const currentPluginModule = pluginModuleMap[pluginName.value as string];
|
||||||
|
|
||||||
|
if (!currentPluginModule) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const callbackFunction =
|
||||||
|
currentPluginModule?.extensionPoints?.["plugin:self:tabs:create"];
|
||||||
|
|
||||||
|
if (typeof callbackFunction !== "function") {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const pluginTabs = await callbackFunction();
|
||||||
|
|
||||||
|
return pluginTabs.filter((tab) => {
|
||||||
|
return currentUserHasPermission(tab.permissions);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
plugin,
|
||||||
|
setting,
|
||||||
|
tabs,
|
||||||
|
activeTab,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -6,9 +6,12 @@ import type { RouteRecordRaw } from "vue-router";
|
||||||
import PluginDetail from "./PluginDetail.vue";
|
import PluginDetail from "./PluginDetail.vue";
|
||||||
import PluginExtensionPointSettings from "./PluginExtensionPointSettings.vue";
|
import PluginExtensionPointSettings from "./PluginExtensionPointSettings.vue";
|
||||||
import PluginList from "./PluginList.vue";
|
import PluginList from "./PluginList.vue";
|
||||||
|
import PluginDetailModal from "./components/PluginDetailModal.vue";
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
components: {},
|
components: {
|
||||||
|
PluginDetailModal,
|
||||||
|
},
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
path: "/plugins",
|
path: "/plugins",
|
||||||
|
|
Loading…
Reference in New Issue