refactor: use tanstack query to refactor setting-form-related fetching of theme and plugin (#3604)

#### What type of PR is this?

/kind improvement

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

使用 [TanStack Query](https://github.com/TanStack/query) 重构主题和插件设置表单的逻辑,移除无意义的重复请求。

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

Ref https://github.com/halo-dev/halo/issues/3360

#### Special notes for your reviewer:

测试方式:

1. 安装若干带有设置表单的主题和插件。
2. 测试主题和插件的设置表单是否加载正常,以及保存设置后再重复加载的时候是否正常。

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

```release-note
None
```
pull/3617/head^2
Ryan Wang 2023-03-29 21:26:13 +08:00 committed by GitHub
parent ad6ac87d73
commit 9814053d0e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 159 additions and 176 deletions

View File

@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
// core libs // core libs
import { inject, ref, watch } from "vue"; import { inject, ref, computed } from "vue";
// components // components
import { Toast, VButton } from "@halo-dev/components"; import { Toast, VButton } from "@halo-dev/components";
@ -14,16 +14,31 @@ import { useRouteParams } from "@vueuse/router";
import { apiClient } from "@/utils/api-client"; import { apiClient } from "@/utils/api-client";
import { useSettingFormConvert } from "@/composables/use-setting-form"; import { useSettingFormConvert } from "@/composables/use-setting-form";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { useQuery, useQueryClient } from "@tanstack/vue-query";
const { t } = useI18n(); const { t } = useI18n();
const queryClient = useQueryClient();
const group = useRouteParams<string>("group"); const group = useRouteParams<string>("group");
const selectedTheme = inject<Ref<Theme | undefined>>("selectedTheme"); const selectedTheme = inject<Ref<Theme | undefined>>("selectedTheme");
const setting = inject<Ref<Setting | undefined>>("setting", ref());
const saving = ref(false); const saving = ref(false);
const setting = ref<Setting>();
const configMap = ref<ConfigMap>(); const { data: configMap, suspense } = useQuery<ConfigMap>({
queryKey: ["theme-configMap", selectedTheme],
queryFn: async () => {
const { data } = await apiClient.theme.fetchThemeConfig({
name: selectedTheme?.value?.metadata.name as string,
});
return data;
},
refetchOnWindowFocus: false,
enabled: computed(() => {
return !!setting.value && !!selectedTheme?.value;
}),
});
const { configMapFormData, formSchema, convertToSave } = useSettingFormConvert( const { configMapFormData, formSchema, convertToSave } = useSettingFormConvert(
setting, setting,
@ -31,26 +46,6 @@ const { configMapFormData, formSchema, convertToSave } = useSettingFormConvert(
group group
); );
const handleFetchSettings = async () => {
if (!selectedTheme?.value) return;
const { data } = await apiClient.theme.fetchThemeSetting({
name: selectedTheme?.value?.metadata.name,
});
setting.value = data;
};
const handleFetchConfigMap = async () => {
if (!selectedTheme?.value) return;
const { data } = await apiClient.theme.fetchThemeConfig({
name: selectedTheme?.value?.metadata.name,
});
configMap.value = data;
};
const handleSaveConfigMap = async () => { const handleSaveConfigMap = async () => {
saving.value = true; saving.value = true;
@ -61,29 +56,19 @@ const handleSaveConfigMap = async () => {
return; return;
} }
const { data: newConfigMap } = await apiClient.theme.updateThemeConfig({ await apiClient.theme.updateThemeConfig({
name: selectedTheme?.value?.metadata.name, name: selectedTheme?.value?.metadata.name,
configMap: configMapToUpdate, configMap: configMapToUpdate,
}); });
Toast.success(t("core.common.toast.save_success")); Toast.success(t("core.common.toast.save_success"));
await handleFetchSettings(); queryClient.invalidateQueries({ queryKey: ["theme-configMap"] });
configMap.value = newConfigMap;
saving.value = false; saving.value = false;
}; };
await handleFetchSettings(); await suspense();
await handleFetchConfigMap();
watch(
() => selectedTheme?.value,
() => {
handleFetchSettings();
handleFetchConfigMap();
}
);
</script> </script>
<template> <template>
<Transition mode="out-in" name="fade"> <Transition mode="out-in" name="fade">

View File

@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
// core libs // core libs
import { nextTick, onMounted, type Ref } from "vue"; import { nextTick, onMounted, type Ref, computed, watch } from "vue";
import { provide, ref, watch } from "vue"; import { provide, ref } from "vue";
import { useRoute, useRouter } from "vue-router"; import { useRoute, useRouter } from "vue-router";
// libs // libs
@ -33,9 +33,12 @@ import { useThemeStore } from "@/stores/theme";
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
import { apiClient } from "@/utils/api-client"; import { apiClient } from "@/utils/api-client";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { useQuery } from "@tanstack/vue-query";
const { currentUserHasPermission } = usePermission(); const { currentUserHasPermission } = usePermission();
const { t } = useI18n(); const { t } = useI18n();
const route = useRoute();
const router = useRouter();
interface ThemeTab { interface ThemeTab {
id: string; id: string;
@ -60,27 +63,56 @@ const tabs = ref<ThemeTab[]>(cloneDeep(initialTabs));
const selectedTheme = ref<Theme>(); const selectedTheme = ref<Theme>();
const themesModal = ref(false); const themesModal = ref(false);
const previewModal = ref(false); const previewModal = ref(false);
const activeTab = ref(""); const activeTab = ref(tabs.value[0].id);
const { loading, isActivated, handleActiveTheme } = const { loading, isActivated, handleActiveTheme } =
useThemeLifeCycle(selectedTheme); useThemeLifeCycle(selectedTheme);
provide<Ref<Theme | undefined>>("selectedTheme", selectedTheme); provide<Ref<Theme | undefined>>("selectedTheme", selectedTheme);
const route = useRoute(); const { data: setting } = useQuery<Setting>({
const router = useRouter(); queryKey: ["theme-setting", selectedTheme],
queryFn: async () => {
const { data } = await apiClient.theme.fetchThemeSetting({
name: selectedTheme.value?.metadata.name as string,
});
return data;
},
refetchOnWindowFocus: false,
enabled: computed(() => {
return (
!!selectedTheme.value &&
!!selectedTheme.value.spec.settingName &&
currentUserHasPermission(["system:themes:view"])
);
}),
async onSuccess(data) {
if (data) {
const { forms } = data.spec;
tabs.value = [
...tabs.value,
...forms.map((item: SettingForm) => {
return {
id: item.group,
label: item.label || "",
route: {
name: "ThemeSetting",
params: {
group: item.group,
},
},
};
}),
] as ThemeTab[];
}
const setting = ref<Setting>(); await nextTick();
const handleFetchSettings = async () => { handleTriggerTabChange();
if (!selectedTheme.value) return; },
});
const { data } = await apiClient.theme.fetchThemeSetting({ provide<Ref<Setting | undefined>>("setting", setting);
name: selectedTheme.value?.metadata.name,
});
setting.value = data;
};
const handleTabChange = (id: string) => { const handleTabChange = (id: string) => {
const tab = tabs.value.find((item) => item.id === id); const tab = tabs.value.find((item) => item.id === id);
@ -90,46 +122,6 @@ const handleTabChange = (id: string) => {
} }
}; };
watch(
() => selectedTheme.value,
async () => {
if (selectedTheme.value) {
// reset tabs
tabs.value = cloneDeep(initialTabs);
if (!currentUserHasPermission(["system:themes:view"])) {
handleTriggerTabChange();
return;
}
await handleFetchSettings();
if (setting.value) {
const { forms } = setting.value.spec;
tabs.value = [
...tabs.value,
...forms.map((item: SettingForm) => {
return {
id: item.group,
label: item.label || "",
route: {
name: "ThemeSetting",
params: {
group: item.group,
},
},
};
}),
] as ThemeTab[];
}
await nextTick();
handleTriggerTabChange();
}
}
);
const handleTriggerTabChange = () => { const handleTriggerTabChange = () => {
if (route.name === "ThemeSetting") { if (route.name === "ThemeSetting") {
const tab = tabs.value.find((tab) => { const tab = tabs.value.find((tab) => {
@ -150,9 +142,10 @@ const handleTriggerTabChange = () => {
activeTab.value = tab ? tab.id : tabs.value[0].id; activeTab.value = tab ? tab.id : tabs.value[0].id;
}; };
watch([() => route.name, () => route.params], async () => { const onSelectTheme = () => {
handleTriggerTabChange(); tabs.value = cloneDeep(initialTabs);
}); handleTabChange(tabs.value[0].id);
};
onMounted(() => { onMounted(() => {
const themeStore = useThemeStore(); const themeStore = useThemeStore();
@ -161,12 +154,17 @@ onMounted(() => {
selectedTheme.value = activatedTheme?.value; selectedTheme.value = activatedTheme?.value;
}); });
watch([() => route.name, () => route.params], async () => {
handleTriggerTabChange();
});
</script> </script>
<template> <template>
<BasicLayout> <BasicLayout>
<ThemeListModal <ThemeListModal
v-model:selected-theme="selectedTheme" v-model:selected-theme="selectedTheme"
v-model:visible="themesModal" v-model:visible="themesModal"
@select="onSelectTheme"
/> />
<VPageHeader :title="selectedTheme?.spec.displayName"> <VPageHeader :title="selectedTheme?.spec.displayName">
<template #icon> <template #icon>
@ -232,7 +230,10 @@ onMounted(() => {
></VTabbar> ></VTabbar>
</template> </template>
<div class="bg-white"> <div class="bg-white">
<RouterView :key="activeTab" v-slot="{ Component }"> <RouterView
:key="`${selectedTheme?.metadata.name}-${activeTab}`"
v-slot="{ Component }"
>
<template v-if="Component"> <template v-if="Component">
<Suspense> <Suspense>
<component :is="Component"></component> <component :is="Component"></component>

View File

@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
// core libs // core libs
import { inject, ref, type Ref } from "vue"; import { inject, ref, type Ref, computed } from "vue";
// hooks // hooks
import { useSettingFormConvert } from "@/composables/use-setting-form"; import { useSettingFormConvert } from "@/composables/use-setting-form";
@ -13,15 +13,30 @@ import { Toast, VButton } from "@halo-dev/components";
import type { ConfigMap, Plugin, Setting } from "@halo-dev/api-client"; import type { ConfigMap, Plugin, Setting } from "@halo-dev/api-client";
import { useRouteParams } from "@vueuse/router"; import { useRouteParams } from "@vueuse/router";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { useQuery, useQueryClient } from "@tanstack/vue-query";
const { t } = useI18n(); const { t } = useI18n();
const queryClient = useQueryClient();
const group = useRouteParams<string>("group"); const group = useRouteParams<string>("group");
const plugin = inject<Ref<Plugin | undefined>>("plugin"); const plugin = inject<Ref<Plugin | undefined>>("plugin");
const setting = inject<Ref<Setting | undefined>>("setting", ref());
const saving = ref(false); const saving = ref(false);
const setting = ref<Setting>();
const configMap = ref<ConfigMap>(); const { data: configMap, suspense } = useQuery<ConfigMap>({
queryKey: ["plugin-configMap", plugin],
queryFn: async () => {
const { data } = await apiClient.plugin.fetchPluginConfig({
name: plugin?.value?.metadata.name as string,
});
return data;
},
refetchOnWindowFocus: false,
enabled: computed(() => {
return !!setting.value && !!plugin?.value;
}),
});
const { configMapFormData, formSchema, convertToSave } = useSettingFormConvert( const { configMapFormData, formSchema, convertToSave } = useSettingFormConvert(
setting, setting,
@ -29,22 +44,6 @@ const { configMapFormData, formSchema, convertToSave } = useSettingFormConvert(
group group
); );
const handleFetchSettings = async () => {
if (!plugin?.value) return;
const { data } = await apiClient.plugin.fetchPluginSetting({
name: plugin.value.metadata.name,
});
setting.value = data;
};
const handleFetchConfigMap = async () => {
if (!plugin?.value) return;
const { data } = await apiClient.plugin.fetchPluginConfig({
name: plugin.value.metadata.name,
});
configMap.value = data;
};
const handleSaveConfigMap = async () => { const handleSaveConfigMap = async () => {
saving.value = true; saving.value = true;
const configMapToUpdate = convertToSave(); const configMapToUpdate = convertToSave();
@ -53,27 +52,26 @@ const handleSaveConfigMap = async () => {
return; return;
} }
const { data: newConfigMap } = await apiClient.plugin.updatePluginConfig({ await apiClient.plugin.updatePluginConfig({
name: plugin.value.metadata.name, name: plugin.value.metadata.name,
configMap: configMapToUpdate, configMap: configMapToUpdate,
}); });
Toast.success(t("core.common.toast.save_success")); Toast.success(t("core.common.toast.save_success"));
await handleFetchSettings(); queryClient.invalidateQueries({ queryKey: ["plugin-configMap"] });
configMap.value = newConfigMap;
saving.value = false; saving.value = false;
}; };
await handleFetchSettings(); await suspense();
await handleFetchConfigMap();
</script> </script>
<template> <template>
<Transition mode="out-in" name="fade"> <Transition mode="out-in" name="fade">
<div class="bg-white p-4"> <div class="bg-white p-4">
<div> <div>
<FormKit <FormKit
v-if="group && formSchema && configMapFormData" v-if="group && formSchema && configMapFormData?.[group]"
:id="group" :id="group"
v-model="configMapFormData[group]" v-model="configMapFormData[group]"
:name="group" :name="group"

View File

@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
// core libs // core libs
import { nextTick, onMounted, provide, ref, watch } from "vue"; import { nextTick, provide, ref, computed, watch } from "vue";
import { RouterView, useRoute, useRouter } from "vue-router"; import { RouterView, useRoute, useRouter } from "vue-router";
import { apiClient } from "@/utils/api-client"; import { apiClient } from "@/utils/api-client";
@ -22,6 +22,7 @@ import type { Ref } from "vue";
import type { Plugin, Setting, SettingForm } from "@halo-dev/api-client"; 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 { useI18n } from "vue-i18n";
import { useQuery } from "@tanstack/vue-query";
const { currentUserHasPermission } = usePermission(); const { currentUserHasPermission } = usePermission();
const { t } = useI18n(); const { t } = useI18n();
@ -48,33 +49,68 @@ const initialTabs: PluginTab[] = [
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const plugin = ref<Plugin>();
const setting = ref<Setting>();
const tabs = ref<PluginTab[]>(cloneDeep(initialTabs)); const tabs = ref<PluginTab[]>(cloneDeep(initialTabs));
const activeTab = ref<string>(); const activeTab = ref<string>(tabs.value[0].id);
provide<Ref<Plugin | undefined>>("plugin", plugin);
provide<Ref<string | undefined>>("activeTab", activeTab); provide<Ref<string | undefined>>("activeTab", activeTab);
const handleFetchPlugin = async () => { const { data: plugin } = useQuery({
try { queryKey: ["plugin", route.params.name],
const response = queryFn: async () => {
const { data } =
await apiClient.extension.plugin.getpluginHaloRunV1alpha1Plugin({ await apiClient.extension.plugin.getpluginHaloRunV1alpha1Plugin({
name: route.params.name as string, name: route.params.name as string,
}); });
plugin.value = response.data; return data;
} catch (e) { },
console.error(e); refetchOnWindowFocus: false,
} });
};
const handleFetchSettings = async () => { provide<Ref<Plugin | undefined>>("plugin", plugin);
if (!plugin.value || !plugin.value.spec.settingName) return;
const { data } = await apiClient.plugin.fetchPluginSetting({ const { data: setting } = useQuery({
name: plugin.value?.metadata.name, queryKey: ["plugin-setting", plugin],
}); queryFn: async () => {
setting.value = data; const { data } = await apiClient.plugin.fetchPluginSetting({
}; name: plugin.value?.metadata.name as string,
});
return data;
},
refetchOnWindowFocus: false,
enabled: computed(() => {
return (
!!plugin.value &&
!!plugin.value.spec.settingName &&
currentUserHasPermission(["system:plugins:manage"])
);
}),
async onSuccess(data) {
if (data) {
const { forms } = data.spec;
tabs.value = [
...tabs.value,
...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 handleTabChange = (id: string) => {
const tab = tabs.value.find((item) => item.id === id); const tab = tabs.value.find((item) => item.id === id);
@ -103,42 +139,6 @@ const handleTriggerTabChange = () => {
activeTab.value = tab ? tab.id : tabs.value[0].id; activeTab.value = tab ? tab.id : tabs.value[0].id;
}; };
onMounted(async () => {
await handleFetchPlugin();
if (!currentUserHasPermission(["system:plugins:manage"])) {
handleTriggerTabChange();
return;
}
await handleFetchSettings();
tabs.value = cloneDeep(initialTabs);
if (setting.value) {
const { forms } = setting.value.spec;
tabs.value = [
...tabs.value,
...forms.map((item: SettingForm) => {
return {
id: item.group,
label: item.label || "",
route: {
name: "PluginSetting",
params: {
group: item.group,
},
},
};
}),
] as PluginTab[];
}
await nextTick();
handleTriggerTabChange();
});
watch([() => route.name, () => route.params], () => { watch([() => route.name, () => route.params], () => {
handleTriggerTabChange(); handleTriggerTabChange();
}); });

View File

@ -22,7 +22,6 @@ import {
VEmpty, VEmpty,
VDropdown, VDropdown,
VDropdownItem, VDropdownItem,
VDropdownDivider,
} from "@halo-dev/components"; } from "@halo-dev/components";
import UserEditingModal from "./components/UserEditingModal.vue"; import UserEditingModal from "./components/UserEditingModal.vue";
import UserPasswordChangeModal from "./components/UserPasswordChangeModal.vue"; import UserPasswordChangeModal from "./components/UserPasswordChangeModal.vue";