mirror of https://github.com/halo-dev/halo
refactor: use tanstack query to refactor menu related data fetching (#3714)
#### 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 Fixed https://github.com/halo-dev/halo/issues/3467 #### Special notes for your reviewer: 测试方式: 1. 创建若干菜单和菜单项,检查功能是否正常。 #### Does this PR introduce a user-facing change? ```release-note 修复 Console 端菜单项和菜单关联可能发现混乱的问题。 ```pull/3756/head^2
parent
8755c24b11
commit
eb141e4966
|
@ -14,7 +14,7 @@ import {
|
||||||
import MenuItemEditingModal from "./components/MenuItemEditingModal.vue";
|
import MenuItemEditingModal from "./components/MenuItemEditingModal.vue";
|
||||||
import MenuItemListItem from "./components/MenuItemListItem.vue";
|
import MenuItemListItem from "./components/MenuItemListItem.vue";
|
||||||
import MenuList from "./components/MenuList.vue";
|
import MenuList from "./components/MenuList.vue";
|
||||||
import { onUnmounted, ref } from "vue";
|
import { computed, ref } from "vue";
|
||||||
import { apiClient } from "@/utils/api-client";
|
import { apiClient } from "@/utils/api-client";
|
||||||
import type { Menu, MenuItem } from "@halo-dev/api-client";
|
import type { Menu, MenuItem } from "@halo-dev/api-client";
|
||||||
import cloneDeep from "lodash.clonedeep";
|
import cloneDeep from "lodash.clonedeep";
|
||||||
|
@ -27,68 +27,49 @@ import {
|
||||||
resetMenuItemsTreePriority,
|
resetMenuItemsTreePriority,
|
||||||
} from "./utils";
|
} from "./utils";
|
||||||
import { useDebounceFn } from "@vueuse/core";
|
import { useDebounceFn } from "@vueuse/core";
|
||||||
import { onBeforeRouteLeave } from "vue-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 menuItems = ref<MenuItem[]>([] as MenuItem[]);
|
|
||||||
const menuTreeItems = ref<MenuTreeItem[]>([] as MenuTreeItem[]);
|
const menuTreeItems = ref<MenuTreeItem[]>([] as MenuTreeItem[]);
|
||||||
const selectedMenu = ref<Menu>();
|
const selectedMenu = ref<Menu>();
|
||||||
const selectedMenuItem = ref<MenuItem>();
|
const selectedMenuItem = ref<MenuItem>();
|
||||||
const selectedParentMenuItem = ref<MenuItem>();
|
const selectedParentMenuItem = ref<MenuItem>();
|
||||||
const loading = ref(false);
|
|
||||||
const menuListRef = ref();
|
|
||||||
const menuItemEditingModal = ref();
|
const menuItemEditingModal = ref();
|
||||||
const refreshInterval = ref();
|
|
||||||
|
|
||||||
const handleFetchMenuItems = async (options?: { mute?: boolean }) => {
|
|
||||||
try {
|
|
||||||
clearInterval(refreshInterval.value);
|
|
||||||
|
|
||||||
if (!options?.mute) {
|
|
||||||
loading.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: menuItems,
|
||||||
|
isLoading,
|
||||||
|
refetch,
|
||||||
|
} = useQuery<MenuItem[]>({
|
||||||
|
queryKey: ["menu-items", selectedMenu],
|
||||||
|
queryFn: async () => {
|
||||||
if (!selectedMenu.value?.spec.menuItems) {
|
if (!selectedMenu.value?.spec.menuItems) {
|
||||||
return;
|
return [];
|
||||||
}
|
}
|
||||||
const menuItemNames = Array.from(selectedMenu.value.spec.menuItems)?.map(
|
|
||||||
(item) => item
|
const menuItemNames = selectedMenu.value.spec.menuItems.filter(Boolean);
|
||||||
);
|
|
||||||
const { data } = await apiClient.extension.menuItem.listv1alpha1MenuItem({
|
const { data } = await apiClient.extension.menuItem.listv1alpha1MenuItem({
|
||||||
page: 0,
|
page: 0,
|
||||||
size: 0,
|
size: 0,
|
||||||
fieldSelector: [`name=(${menuItemNames.join(",")})`],
|
fieldSelector: [`name=(${menuItemNames.join(",")})`],
|
||||||
});
|
});
|
||||||
menuItems.value = data.items;
|
|
||||||
// Build the menu tree
|
|
||||||
menuTreeItems.value = buildMenuItemsTree(data.items);
|
|
||||||
|
|
||||||
const deletedMenuItems = menuItems.value.filter(
|
return data.items;
|
||||||
|
},
|
||||||
|
onSuccess(data) {
|
||||||
|
menuTreeItems.value = buildMenuItemsTree(data);
|
||||||
|
},
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
refetchInterval(data) {
|
||||||
|
const deletingMenuItems = data?.filter(
|
||||||
(menuItem) => !!menuItem.metadata.deletionTimestamp
|
(menuItem) => !!menuItem.metadata.deletionTimestamp
|
||||||
);
|
);
|
||||||
|
return deletingMenuItems?.length ? 3000 : false;
|
||||||
if (deletedMenuItems.length) {
|
},
|
||||||
refreshInterval.value = setInterval(() => {
|
enabled: computed(() => !!selectedMenu.value),
|
||||||
handleFetchMenuItems({ mute: true });
|
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
|
|
||||||
await handleResetMenuItems();
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Failed to fetch menu items", e);
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
clearInterval(refreshInterval.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
onBeforeRouteLeave(() => {
|
|
||||||
clearInterval(refreshInterval.value);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleOpenEditingModal = (menuItem: MenuTreeItem) => {
|
const handleOpenEditingModal = (menuItem: MenuTreeItem) => {
|
||||||
|
@ -120,11 +101,10 @@ const onMenuItemSaved = async (menuItem: MenuItem) => {
|
||||||
menuToUpdate &&
|
menuToUpdate &&
|
||||||
!menuToUpdate.spec.menuItems?.includes(menuItem.metadata.name)
|
!menuToUpdate.spec.menuItems?.includes(menuItem.metadata.name)
|
||||||
) {
|
) {
|
||||||
if (menuToUpdate.spec.menuItems) {
|
menuToUpdate.spec.menuItems = [
|
||||||
menuToUpdate.spec.menuItems.push(menuItem.metadata.name);
|
...(menuToUpdate.spec.menuItems || []),
|
||||||
} else {
|
menuItem.metadata.name,
|
||||||
menuToUpdate.spec.menuItems = [menuItem.metadata.name];
|
];
|
||||||
}
|
|
||||||
|
|
||||||
await apiClient.extension.menu.updatev1alpha1Menu({
|
await apiClient.extension.menu.updatev1alpha1Menu({
|
||||||
name: menuToUpdate.metadata.name,
|
name: menuToUpdate.metadata.name,
|
||||||
|
@ -132,8 +112,8 @@ const onMenuItemSaved = async (menuItem: MenuItem) => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await menuListRef.value.handleFetchMenus();
|
await queryClient.invalidateQueries({ queryKey: ["menus"] });
|
||||||
await handleFetchMenuItems({ mute: true });
|
await refetch();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdateInBatch = useDebounceFn(async () => {
|
const handleUpdateInBatch = useDebounceFn(async () => {
|
||||||
|
@ -150,8 +130,8 @@ const handleUpdateInBatch = useDebounceFn(async () => {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to update menu items", e);
|
console.error("Failed to update menu items", e);
|
||||||
} finally {
|
} finally {
|
||||||
await menuListRef.value.handleFetchMenus();
|
await queryClient.invalidateQueries({ queryKey: ["menus"] });
|
||||||
await handleFetchMenuItems({ mute: true });
|
await refetch();
|
||||||
}
|
}
|
||||||
}, 300);
|
}, 300);
|
||||||
|
|
||||||
|
@ -178,32 +158,25 @@ const handleDelete = async (menuItem: MenuTreeItem) => {
|
||||||
await Promise.all(deleteChildrenRequests);
|
await Promise.all(deleteChildrenRequests);
|
||||||
}
|
}
|
||||||
|
|
||||||
await handleFetchMenuItems();
|
await refetch();
|
||||||
|
|
||||||
Toast.success(t("core.common.toast.delete_success"));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleResetMenuItems = async () => {
|
|
||||||
if (!selectedMenu.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// update items under menu
|
||||||
const menuToUpdate = cloneDeep(selectedMenu.value);
|
const menuToUpdate = cloneDeep(selectedMenu.value);
|
||||||
|
if (menuToUpdate) {
|
||||||
const menuItemNames = menuItems.value.map((menuItem) => {
|
menuToUpdate.spec.menuItems = menuToUpdate.spec.menuItems?.filter(
|
||||||
return menuItem.metadata.name;
|
(name) => ![menuItem.metadata.name, ...childrenNames].includes(name)
|
||||||
});
|
);
|
||||||
|
|
||||||
menuToUpdate.spec.menuItems = menuItemNames;
|
|
||||||
|
|
||||||
await apiClient.extension.menu.updatev1alpha1Menu({
|
await apiClient.extension.menu.updatev1alpha1Menu({
|
||||||
name: menuToUpdate.metadata.name,
|
name: menuToUpdate.metadata.name,
|
||||||
menu: menuToUpdate,
|
menu: menuToUpdate,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await menuListRef.value.handleFetchMenus({ mute: true });
|
await queryClient.invalidateQueries({ queryKey: ["menus"] });
|
||||||
|
|
||||||
|
Toast.success(t("core.common.toast.delete_success"));
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
|
@ -223,11 +196,7 @@ const handleResetMenuItems = async () => {
|
||||||
<div class="m-0 md:m-4">
|
<div class="m-0 md:m-4">
|
||||||
<div class="flex flex-col gap-4 sm:flex-row">
|
<div class="flex flex-col gap-4 sm:flex-row">
|
||||||
<div class="w-96">
|
<div class="w-96">
|
||||||
<MenuList
|
<MenuList v-model:selected-menu="selectedMenu" @select="refetch()" />
|
||||||
ref="menuListRef"
|
|
||||||
v-model:selected-menu="selectedMenu"
|
|
||||||
@select="handleFetchMenuItems()"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<VCard :body-class="['!p-0']">
|
<VCard :body-class="['!p-0']">
|
||||||
|
@ -256,15 +225,15 @@ const handleResetMenuItems = async () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<VLoading v-if="loading" />
|
<VLoading v-if="isLoading" />
|
||||||
<Transition v-else-if="!menuItems.length" appear name="fade">
|
<Transition v-else-if="!menuItems?.length" appear name="fade">
|
||||||
<VEmpty
|
<VEmpty
|
||||||
:message="$t('core.menu.menu_item_empty.message')"
|
:message="$t('core.menu.menu_item_empty.message')"
|
||||||
:title="$t('core.menu.menu_item_empty.title')"
|
:title="$t('core.menu.menu_item_empty.title')"
|
||||||
>
|
>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<VSpace>
|
<VSpace>
|
||||||
<VButton @click="handleFetchMenuItems()">
|
<VButton @click="refetch()">
|
||||||
{{ $t("core.common.buttons.refresh") }}
|
{{ $t("core.common.buttons.refresh") }}
|
||||||
</VButton>
|
</VButton>
|
||||||
<VButton
|
<VButton
|
||||||
|
|
|
@ -14,23 +14,23 @@ import {
|
||||||
VDropdownItem,
|
VDropdownItem,
|
||||||
} from "@halo-dev/components";
|
} from "@halo-dev/components";
|
||||||
import MenuEditingModal from "./MenuEditingModal.vue";
|
import MenuEditingModal from "./MenuEditingModal.vue";
|
||||||
import { onMounted, onUnmounted, ref } from "vue";
|
import { onMounted, ref } from "vue";
|
||||||
import type { Menu } from "@halo-dev/api-client";
|
import type { Menu } from "@halo-dev/api-client";
|
||||||
import { apiClient } from "@/utils/api-client";
|
import { apiClient } from "@/utils/api-client";
|
||||||
import { useRouteQuery } from "@vueuse/router";
|
import { useRouteQuery } from "@vueuse/router";
|
||||||
import { usePermission } from "@/utils/permission";
|
import { usePermission } from "@/utils/permission";
|
||||||
import { onBeforeRouteLeave } from "vue-router";
|
|
||||||
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 props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
selectedMenu: Menu | null;
|
selectedMenu?: Menu;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
selectedMenu: null,
|
selectedMenu: undefined,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -39,56 +39,39 @@ const emit = defineEmits<{
|
||||||
(event: "update:selectedMenu", menu: Menu): void;
|
(event: "update:selectedMenu", menu: Menu): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const menus = ref<Menu[]>([] as Menu[]);
|
|
||||||
const loading = ref(false);
|
|
||||||
const selectedMenuToUpdate = ref<Menu>();
|
const selectedMenuToUpdate = ref<Menu>();
|
||||||
const menuEditingModal = ref<boolean>(false);
|
const menuEditingModal = ref<boolean>(false);
|
||||||
const refreshInterval = ref();
|
|
||||||
|
|
||||||
const handleFetchMenus = async (options?: { mute?: boolean }) => {
|
const {
|
||||||
selectedMenuToUpdate.value = undefined;
|
data: menus,
|
||||||
try {
|
isLoading,
|
||||||
clearInterval(refreshInterval.value);
|
refetch,
|
||||||
|
} = useQuery<Menu[]>({
|
||||||
if (!options?.mute) {
|
queryKey: ["menus"],
|
||||||
loading.value = true;
|
queryFn: async () => {
|
||||||
}
|
const { data } = await apiClient.extension.menu.listv1alpha1Menu({
|
||||||
|
page: 0,
|
||||||
const { data } = await apiClient.extension.menu.listv1alpha1Menu();
|
size: 0,
|
||||||
menus.value = data.items;
|
});
|
||||||
|
return data.items;
|
||||||
// update selected menu
|
},
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
onSuccess(data) {
|
||||||
if (props.selectedMenu) {
|
if (props.selectedMenu) {
|
||||||
const updatedMenu = menus.value.find(
|
const updatedMenu = data?.find(
|
||||||
(menu) => menu.metadata.name === props.selectedMenu?.metadata.name
|
(menu) => menu.metadata.name === props.selectedMenu?.metadata.name
|
||||||
);
|
);
|
||||||
if (updatedMenu) {
|
if (updatedMenu) {
|
||||||
emit("update:selectedMenu", updatedMenu);
|
emit("update:selectedMenu", updatedMenu);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
const deletedMenus = menus.value.filter(
|
refetchInterval(data) {
|
||||||
|
const deletingMenus = data?.filter(
|
||||||
(menu) => !!menu.metadata.deletionTimestamp
|
(menu) => !!menu.metadata.deletionTimestamp
|
||||||
);
|
);
|
||||||
|
return deletingMenus?.length ? 3000 : false;
|
||||||
if (deletedMenus.length) {
|
},
|
||||||
refreshInterval.value = setInterval(() => {
|
|
||||||
handleFetchMenus({ mute: true });
|
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Failed to fetch menus", e);
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
clearInterval(refreshInterval.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
onBeforeRouteLeave(() => {
|
|
||||||
clearInterval(refreshInterval.value);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const menuQuery = useRouteQuery("menu");
|
const menuQuery = useRouteQuery("menu");
|
||||||
|
@ -111,20 +94,23 @@ const handleDeleteMenu = async (menu: Menu) => {
|
||||||
name: menu.metadata.name,
|
name: menu.metadata.name,
|
||||||
});
|
});
|
||||||
|
|
||||||
const deleteItemsPromises = Array.from(menu.spec.menuItems || []).map(
|
const deleteItemsPromises = menu.spec.menuItems?.map((item) =>
|
||||||
(item) =>
|
|
||||||
apiClient.extension.menuItem.deletev1alpha1MenuItem({
|
apiClient.extension.menuItem.deletev1alpha1MenuItem({
|
||||||
name: item,
|
name: item,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!deleteItemsPromises) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await Promise.all(deleteItemsPromises);
|
await Promise.all(deleteItemsPromises);
|
||||||
|
|
||||||
Toast.success(t("core.common.toast.delete_success"));
|
Toast.success(t("core.common.toast.delete_success"));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to delete menu", e);
|
console.error("Failed to delete menu", e);
|
||||||
} finally {
|
} finally {
|
||||||
await handleFetchMenus();
|
await refetch();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -136,41 +122,39 @@ const handleOpenEditingModal = (menu?: Menu) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await handleFetchMenus();
|
await refetch();
|
||||||
|
|
||||||
if (menuQuery.value) {
|
if (menuQuery.value) {
|
||||||
const menu = menus.value.find((m) => m.metadata.name === menuQuery.value);
|
const menu = menus.value?.find((m) => m.metadata.name === menuQuery.value);
|
||||||
if (menu) {
|
if (menu) {
|
||||||
handleSelect(menu);
|
handleSelect(menu);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (menus.value.length > 0) {
|
if (menus.value?.length) {
|
||||||
handleSelect(menus.value[0]);
|
handleSelect(menus.value[0]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
defineExpose({
|
|
||||||
handleFetchMenus,
|
|
||||||
});
|
|
||||||
|
|
||||||
// primary menu
|
// primary menu
|
||||||
const primaryMenuName = ref<string>();
|
const { data: primaryMenuName, refetch: refetchPrimaryMenuName } = useQuery({
|
||||||
|
queryKey: ["primary-menu-name"],
|
||||||
const handleFetchPrimaryMenuName = async () => {
|
queryFn: async () => {
|
||||||
const { data } = await apiClient.extension.configMap.getv1alpha1ConfigMap({
|
const { data } = await apiClient.extension.configMap.getv1alpha1ConfigMap({
|
||||||
name: "system",
|
name: "system",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!data.data?.menu) {
|
if (!data.data?.menu) {
|
||||||
return;
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
const menuConfig = JSON.parse(data.data.menu);
|
const menuConfig = JSON.parse(data.data.menu);
|
||||||
|
|
||||||
primaryMenuName.value = menuConfig.primary;
|
return menuConfig.primary;
|
||||||
};
|
},
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
});
|
||||||
|
|
||||||
const handleSetPrimaryMenu = async (menu: Menu) => {
|
const handleSetPrimaryMenu = async (menu: Menu) => {
|
||||||
const { data: systemConfigMap } =
|
const { data: systemConfigMap } =
|
||||||
|
@ -188,30 +172,28 @@ const handleSetPrimaryMenu = async (menu: Menu) => {
|
||||||
configMap: systemConfigMap,
|
configMap: systemConfigMap,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
await handleFetchPrimaryMenuName();
|
await refetchPrimaryMenuName();
|
||||||
|
|
||||||
Toast.success(t("core.menu.operations.set_primary.toast_success"));
|
Toast.success(t("core.menu.operations.set_primary.toast_success"));
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(handleFetchPrimaryMenuName);
|
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<MenuEditingModal
|
<MenuEditingModal
|
||||||
v-model:visible="menuEditingModal"
|
v-model:visible="menuEditingModal"
|
||||||
:menu="selectedMenuToUpdate"
|
:menu="selectedMenuToUpdate"
|
||||||
@close="handleFetchMenus()"
|
@close="refetch()"
|
||||||
@created="handleSelect"
|
@created="handleSelect"
|
||||||
/>
|
/>
|
||||||
<VCard :body-class="['!p-0']" :title="$t('core.menu.title')">
|
<VCard :body-class="['!p-0']" :title="$t('core.menu.title')">
|
||||||
<VLoading v-if="loading" />
|
<VLoading v-if="isLoading" />
|
||||||
<Transition v-else-if="!menus.length" appear name="fade">
|
<Transition v-else-if="!menus?.length" appear name="fade">
|
||||||
<VEmpty
|
<VEmpty
|
||||||
:message="$t('core.menu.empty.message')"
|
:message="$t('core.menu.empty.message')"
|
||||||
:title="$t('core.menu.empty.title')"
|
:title="$t('core.menu.empty.title')"
|
||||||
>
|
>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<VSpace>
|
<VSpace>
|
||||||
<VButton size="sm" @click="handleFetchMenus()">
|
<VButton size="sm" @click="refetch()">
|
||||||
{{ $t("core.common.buttons.refresh") }}
|
{{ $t("core.common.buttons.refresh") }}
|
||||||
</VButton>
|
</VButton>
|
||||||
</VSpace>
|
</VSpace>
|
||||||
|
|
Loading…
Reference in New Issue