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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
<script lang="ts" setup>
// 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 { 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 { usePermission } from "@/utils/permission";
import { useI18n } from "vue-i18n";
import { useQuery } from "@tanstack/vue-query";
const { currentUserHasPermission } = usePermission();
const { t } = useI18n();
@ -48,33 +49,68 @@ const initialTabs: PluginTab[] = [
const route = useRoute();
const router = useRouter();
const plugin = ref<Plugin>();
const setting = ref<Setting>();
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);
const handleFetchPlugin = async () => {
try {
const response =
const { data: plugin } = useQuery({
queryKey: ["plugin", route.params.name],
queryFn: async () => {
const { data } =
await apiClient.extension.plugin.getpluginHaloRunV1alpha1Plugin({
name: route.params.name as string,
});
plugin.value = response.data;
} catch (e) {
console.error(e);
}
};
return data;
},
refetchOnWindowFocus: false,
});
const handleFetchSettings = async () => {
if (!plugin.value || !plugin.value.spec.settingName) return;
const { data } = await apiClient.plugin.fetchPluginSetting({
name: plugin.value?.metadata.name,
});
setting.value = 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;
},
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 tab = tabs.value.find((item) => item.id === id);
@ -103,42 +139,6 @@ const handleTriggerTabChange = () => {
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], () => {
handleTriggerTabChange();
});

View File

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