feat: automatically refresh the list when it has data that is being deleted (halo-dev/console#661)

#### What type of PR is this?

/kind feature
/milestone 2.0

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

优化部分数据列表的逻辑,支持在检测出有正在删除的数据时,自动定时刷新列表。

#### Special notes for your reviewer:

/cc @halo-dev/sig-halo-console 

测试方式:

1. 进入任意一个数据列表,比如文章。
2. 删除一个文章,观察是否有自动刷新列表。
3. 切换路由,检查自动刷新是否停止。

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

```release-note
优化部分数据列表的逻辑,支持在检测出有正在删除的数据时,自动定时刷新列表。
```
pull/3445/head
Ryan Wang 2022-10-24 22:10:10 +08:00 committed by GitHub
parent 9d91adc590
commit 2e60eaee00
12 changed files with 236 additions and 8 deletions

View File

@ -11,6 +11,7 @@ import type { AttachmentLike } from "@halo-dev/console-shared";
import { apiClient } from "@/utils/api-client";
import { Dialog } from "@halo-dev/components";
import type { Content, Editor } from "@halo-dev/richtext-editor";
import { onBeforeRouteLeave } from "vue-router";
interface useAttachmentControlReturn {
attachments: Ref<AttachmentList>;
@ -63,9 +64,12 @@ export function useAttachmentControl(filterOptions?: {
const selectedAttachment = ref<Attachment>();
const selectedAttachments = ref<Set<Attachment>>(new Set<Attachment>());
const checkedAll = ref(false);
const refreshInterval = ref();
const handleFetchAttachments = async () => {
try {
clearInterval(refreshInterval.value);
loading.value = true;
const { data } = await apiClient.attachment.searchAttachments({
policy: policy?.value?.metadata.name,
@ -76,6 +80,16 @@ export function useAttachmentControl(filterOptions?: {
size: attachments.value.size,
});
attachments.value = data;
const deletedAttachments = attachments.value.items.filter(
(attachment) => !!attachment.metadata.deletionTimestamp
);
if (deletedAttachments.length) {
refreshInterval.value = setInterval(() => {
handleFetchAttachments();
}, 3000);
}
} catch (e) {
console.error("Failed to fetch attachments", e);
} finally {
@ -83,6 +97,10 @@ export function useAttachmentControl(filterOptions?: {
}
};
onBeforeRouteLeave(() => {
clearInterval(refreshInterval.value);
});
const handlePaginationChange = async ({
page,
size,

View File

@ -21,6 +21,7 @@ import type {
} from "@halo-dev/api-client";
import { onMounted, ref, watch } from "vue";
import { apiClient } from "@/utils/api-client";
import { onBeforeRouteLeave } from "vue-router";
const comments = ref<ListedCommentList>({
page: 1,
@ -37,9 +38,12 @@ const checkAll = ref(false);
const selectedComment = ref<ListedComment>();
const selectedCommentNames = ref<string[]>([]);
const keyword = ref("");
const refreshInterval = ref();
const handleFetchComments = async () => {
try {
clearInterval(refreshInterval.value);
loading.value = true;
const { data } = await apiClient.comment.listComments({
page: comments.value.page,
@ -47,10 +51,19 @@ const handleFetchComments = async () => {
approved: selectedApprovedFilterItem.value.value,
sort: selectedSortFilterItem.value.value,
keyword: keyword.value,
ownerKind: "User",
ownerName: selectedUser.value?.metadata.name,
});
comments.value = data;
const deletedComments = comments.value.items.filter(
(comment) => !!comment.comment.metadata.deletionTimestamp
);
if (deletedComments.length) {
refreshInterval.value = setInterval(() => {
handleFetchComments();
}, 3000);
}
} catch (error) {
console.log("Failed to fetch comments", error);
} finally {
@ -58,6 +71,10 @@ const handleFetchComments = async () => {
}
};
onBeforeRouteLeave(() => {
clearInterval(refreshInterval.value);
});
const handlePaginationChange = ({
page,
size,

View File

@ -20,10 +20,10 @@ import type {
SinglePage,
} from "@halo-dev/api-client";
import { formatDatetime } from "@/utils/date";
import { computed, provide, ref, watch, type Ref } from "vue";
import { computed, onMounted, provide, ref, watch, type Ref } from "vue";
import ReplyListItem from "./ReplyListItem.vue";
import { apiClient } from "@/utils/api-client";
import type { RouteLocationRaw } from "vue-router";
import { onBeforeRouteLeave, type RouteLocationRaw } from "vue-router";
import cloneDeep from "lodash.clonedeep";
import { usePermission } from "@/utils/permission";
@ -50,6 +50,7 @@ const hoveredReply = ref<ListedReply>();
const loading = ref(false);
const showReplies = ref(false);
const replyModal = ref(false);
const refreshInterval = ref();
provide<Ref<ListedReply | undefined>>("hoveredReply", hoveredReply);
@ -115,11 +116,23 @@ const handleApprove = async () => {
const handleFetchReplies = async () => {
try {
clearInterval(refreshInterval.value);
loading.value = true;
const { data } = await apiClient.reply.listReplies({
commentName: props.comment.comment.metadata.name,
});
replies.value = data.items;
const deletedReplies = replies.value.filter(
(reply) => !!reply.reply.metadata.deletionTimestamp
);
if (deletedReplies.length) {
refreshInterval.value = setInterval(() => {
handleFetchReplies();
}, 3000);
}
} catch (error) {
console.error("Failed to fetch comment replies", error);
} finally {
@ -127,6 +140,14 @@ const handleFetchReplies = async () => {
}
};
onMounted(() => {
clearInterval(refreshInterval.value);
});
onBeforeRouteLeave(() => {
clearInterval(refreshInterval.value);
});
watch(
() => showReplies.value,
(newValue) => {

View File

@ -31,7 +31,7 @@ import type {
} from "@halo-dev/api-client";
import { apiClient } from "@/utils/api-client";
import { formatDatetime } from "@/utils/date";
import { RouterLink } from "vue-router";
import { onBeforeRouteLeave, RouterLink } from "vue-router";
import cloneDeep from "lodash.clonedeep";
import { usePermission } from "@/utils/permission";
@ -58,9 +58,12 @@ const settingModal = ref(false);
const selectedSinglePage = ref<SinglePage>();
const selectedSinglePageWithContent = ref<SinglePageRequest>();
const checkAll = ref(false);
const refreshInterval = ref();
const handleFetchSinglePages = async () => {
try {
clearInterval(refreshInterval.value);
loading.value = true;
let contributors: string[] | undefined;
@ -80,6 +83,16 @@ const handleFetchSinglePages = async () => {
contributor: contributors,
});
singlePages.value = data;
const deletedSinglePages = singlePages.value.items.filter(
(singlePage) => !!singlePage.page.metadata.deletionTimestamp
);
if (deletedSinglePages.length) {
refreshInterval.value = setInterval(() => {
handleFetchSinglePages();
}, 3000);
}
} catch (error) {
console.error("Failed to fetch single pages", error);
} finally {
@ -87,6 +100,10 @@ const handleFetchSinglePages = async () => {
}
};
onBeforeRouteLeave(() => {
clearInterval(refreshInterval.value);
});
const handlePaginationChange = ({
page,
size,

View File

@ -39,6 +39,7 @@ import { formatDatetime } from "@/utils/date";
import { usePostCategory } from "@/modules/contents/posts/categories/composables/use-post-category";
import { usePostTag } from "@/modules/contents/posts/tags/composables/use-post-tag";
import { usePermission } from "@/utils/permission";
import { onBeforeRouteLeave } from "vue-router";
const { currentUserHasPermission } = usePermission();
@ -64,9 +65,12 @@ const selectedPost = ref<Post | null>(null);
const selectedPostWithContent = ref<PostRequest | null>(null);
const checkedAll = ref(false);
const selectedPostNames = ref<string[]>([]);
const refreshInterval = ref();
const handleFetchPosts = async () => {
try {
clearInterval(refreshInterval.value);
loading.value = true;
let categories: string[] | undefined;
@ -101,6 +105,16 @@ const handleFetchPosts = async () => {
contributor: contributors,
});
posts.value = data;
const deletedPosts = posts.value.items.filter(
(post) => !!post.post.metadata.deletionTimestamp
);
if (deletedPosts.length) {
refreshInterval.value = setInterval(() => {
handleFetchPosts();
}, 3000);
}
} catch (e) {
console.error("Failed to fetch posts", e);
} finally {
@ -108,6 +122,10 @@ const handleFetchPosts = async () => {
}
};
onBeforeRouteLeave(() => {
clearInterval(refreshInterval.value);
});
const handlePaginationChange = ({
page,
size,

View File

@ -1,10 +1,11 @@
import { apiClient } from "@/utils/api-client";
import type { Category } from "@halo-dev/api-client";
import type { Ref } from "vue";
import { onUnmounted, type Ref } from "vue";
import { onMounted, ref } from "vue";
import type { CategoryTree } from "@/modules/contents/posts/categories/utils";
import { buildCategoriesTree } from "@/modules/contents/posts/categories/utils";
import { Dialog } from "@halo-dev/components";
import { onBeforeRouteLeave } from "vue-router";
interface usePostCategoryReturn {
categories: Ref<Category[]>;
@ -22,9 +23,12 @@ export function usePostCategory(options?: {
const categories = ref<Category[]>([] as Category[]);
const categoriesTree = ref<CategoryTree[]>([] as CategoryTree[]);
const loading = ref(false);
const refreshInterval = ref();
const handleFetchCategories = async () => {
try {
clearInterval(refreshInterval.value);
loading.value = true;
const { data } =
await apiClient.extension.category.listcontentHaloRunV1alpha1Category({
@ -33,6 +37,16 @@ export function usePostCategory(options?: {
});
categories.value = data.items;
categoriesTree.value = buildCategoriesTree(data.items);
const deletedCategories = categories.value.filter(
(category) => !!category.metadata.deletionTimestamp
);
if (deletedCategories.length) {
refreshInterval.value = setInterval(() => {
handleFetchCategories();
}, 3000);
}
} catch (e) {
console.error("Failed to fetch categories", e);
} finally {
@ -40,6 +54,14 @@ export function usePostCategory(options?: {
}
};
onUnmounted(() => {
clearInterval(refreshInterval.value);
});
onBeforeRouteLeave(() => {
clearInterval(refreshInterval.value);
});
const handleDelete = async (category: CategoryTree) => {
Dialog.warning({
title: "确定要删除该分类吗?",

View File

@ -1,8 +1,9 @@
import { apiClient } from "@/utils/api-client";
import type { Tag } from "@halo-dev/api-client";
import type { Ref } from "vue";
import { onUnmounted, type Ref } from "vue";
import { onMounted, ref } from "vue";
import { Dialog } from "@halo-dev/components";
import { onBeforeRouteLeave } from "vue-router";
interface usePostTagReturn {
tags: Ref<Tag[]>;
@ -18,9 +19,12 @@ export function usePostTag(options?: {
const tags = ref<Tag[]>([] as Tag[]);
const loading = ref(false);
const refreshInterval = ref();
const handleFetchTags = async () => {
try {
clearInterval(refreshInterval.value);
loading.value = true;
const { data } =
await apiClient.extension.tag.listcontentHaloRunV1alpha1Tag({
@ -29,6 +33,16 @@ export function usePostTag(options?: {
});
tags.value = data.items;
const deletedTags = tags.value.filter(
(tag) => !!tag.metadata.deletionTimestamp
);
if (deletedTags.length) {
refreshInterval.value = setInterval(() => {
handleFetchTags();
}, 3000);
}
} catch (e) {
console.error("Failed to fetch tags", e);
} finally {
@ -36,6 +50,14 @@ export function usePostTag(options?: {
}
};
onUnmounted(() => {
clearInterval(refreshInterval.value);
});
onBeforeRouteLeave(() => {
clearInterval(refreshInterval.value);
});
const handleDelete = async (tag: Tag) => {
Dialog.warning({
title: "确定要删除该标签吗?",

View File

@ -12,7 +12,7 @@ import {
import MenuItemEditingModal from "./components/MenuItemEditingModal.vue";
import MenuItemListItem from "./components/MenuItemListItem.vue";
import MenuList from "./components/MenuList.vue";
import { ref } from "vue";
import { onUnmounted, ref } from "vue";
import { apiClient } from "@/utils/api-client";
import type { Menu, MenuItem } from "@halo-dev/api-client";
import cloneDeep from "lodash.clonedeep";
@ -25,6 +25,7 @@ import {
resetMenuItemsTreePriority,
} from "./utils";
import { useDebounceFn } from "@vueuse/core";
import { onBeforeRouteLeave } from "vue-router";
const menuItems = ref<MenuItem[]>([] as MenuItem[]);
const menuTreeItems = ref<MenuTreeItem[]>([] as MenuTreeItem[]);
@ -34,9 +35,12 @@ const selectedParentMenuItem = ref<MenuItem>();
const loading = ref(false);
const menuListRef = ref();
const menuItemEditingModal = ref();
const refreshInterval = ref();
const handleFetchMenuItems = async () => {
try {
clearInterval(refreshInterval.value);
loading.value = true;
if (!selectedMenu.value?.spec.menuItems) {
@ -53,6 +57,16 @@ const handleFetchMenuItems = async () => {
menuItems.value = data.items;
// Build the menu tree
menuTreeItems.value = buildMenuItemsTree(data.items);
const deletedMenuItems = menuItems.value.filter(
(menuItem) => !!menuItem.metadata.deletionTimestamp
);
if (deletedMenuItems.length) {
refreshInterval.value = setInterval(() => {
handleFetchMenuItems();
}, 3000);
}
} catch (e) {
console.error("Failed to fetch menu items", e);
} finally {
@ -60,6 +74,14 @@ const handleFetchMenuItems = async () => {
}
};
onUnmounted(() => {
clearInterval(refreshInterval.value);
});
onBeforeRouteLeave(() => {
clearInterval(refreshInterval.value);
});
const handleOpenEditingModal = (menuItem: MenuTreeItem) => {
selectedMenuItem.value = convertMenuTreeItemToMenuItem(menuItem);
menuItemEditingModal.value = true;

View File

@ -10,11 +10,12 @@ import {
VEntityField,
} from "@halo-dev/components";
import MenuEditingModal from "./MenuEditingModal.vue";
import { onMounted, ref } from "vue";
import { onMounted, onUnmounted, ref } from "vue";
import type { Menu } from "@halo-dev/api-client";
import { apiClient } from "@/utils/api-client";
import { useRouteQuery } from "@vueuse/router";
import { usePermission } from "@/utils/permission";
import { onBeforeRouteLeave } from "vue-router";
const { currentUserHasPermission } = usePermission();
@ -36,10 +37,13 @@ const menus = ref<Menu[]>([] as Menu[]);
const loading = ref(false);
const selectedMenuToUpdate = ref<Menu>();
const menuEditingModal = ref<boolean>(false);
const refreshInterval = ref();
const handleFetchMenus = async () => {
selectedMenuToUpdate.value = undefined;
try {
clearInterval(refreshInterval.value);
loading.value = true;
const { data } = await apiClient.extension.menu.listv1alpha1Menu();
@ -54,6 +58,16 @@ const handleFetchMenus = async () => {
emit("update:selectedMenu", updatedMenu);
}
}
const deletedMenus = menus.value.filter(
(menu) => !!menu.metadata.deletionTimestamp
);
if (deletedMenus.length) {
refreshInterval.value = setInterval(() => {
handleFetchMenus();
}, 3000);
}
} catch (e) {
console.error("Failed to fetch menus", e);
} finally {
@ -61,6 +75,14 @@ const handleFetchMenus = async () => {
}
};
onUnmounted(() => {
clearInterval(refreshInterval.value);
});
onBeforeRouteLeave(() => {
clearInterval(refreshInterval.value);
});
const menuQuery = useRouteQuery("menu");
const handleSelect = (menu: Menu) => {
emit("update:selectedMenu", menu);

View File

@ -23,6 +23,7 @@ import { computed, ref, watch } from "vue";
import type { Theme } from "@halo-dev/api-client";
import { apiClient } from "@/utils/api-client";
import { usePermission } from "@/utils/permission";
import { onBeforeRouteLeave } from "vue-router";
const { currentUserHasPermission } = usePermission();
@ -51,6 +52,7 @@ const themes = ref<Theme[]>([] as Theme[]);
const loading = ref(false);
const themeInstall = ref(false);
const creating = ref(false);
const refreshInterval = ref();
const modalTitle = computed(() => {
return activeTab.value === "installed" ? "已安装的主题" : "未安装的主题";
@ -58,13 +60,31 @@ const modalTitle = computed(() => {
const handleFetchThemes = async () => {
try {
clearInterval(refreshInterval.value);
loading.value = true;
const { data } = await apiClient.theme.listThemes({
page: 0,
size: 0,
uninstalled: activeTab.value !== "installed",
page: 0,
size: 0,
});
themes.value = data.items;
if (activeTab.value !== "installed") {
return;
}
const deletedThemes = themes.value.filter(
(theme) => !!theme.metadata.deletionTimestamp
);
if (deletedThemes.length) {
refreshInterval.value = setInterval(() => {
handleFetchThemes();
}, 3000);
}
} catch (e) {
console.error("Failed to fetch themes", e);
} finally {
@ -72,6 +92,10 @@ const handleFetchThemes = async () => {
}
};
onBeforeRouteLeave(() => {
clearInterval(refreshInterval.value);
});
watch(
() => activeTab.value,
() => {
@ -159,6 +183,8 @@ watch(
(visible) => {
if (visible) {
handleFetchThemes();
} else {
clearInterval(refreshInterval.value);
}
}
);

View File

@ -18,6 +18,7 @@ import { onMounted, ref } from "vue";
import { apiClient } from "@/utils/api-client";
import type { PluginList } from "@halo-dev/api-client";
import { usePermission } from "@/utils/permission";
import { onBeforeRouteLeave } from "vue-router";
const { currentUserHasPermission } = usePermission();
@ -34,9 +35,12 @@ const plugins = ref<PluginList>({
const loading = ref(false);
const pluginInstall = ref(false);
const keyword = ref("");
const refreshInterval = ref();
const handleFetchPlugins = async () => {
try {
clearInterval(refreshInterval.value);
loading.value = true;
const { data } = await apiClient.plugin.listPlugins({
@ -50,6 +54,16 @@ const handleFetchPlugins = async () => {
});
plugins.value = data;
const deletedPlugins = plugins.value.items.filter(
(plugin) => !!plugin.metadata.deletionTimestamp
);
if (deletedPlugins.length) {
refreshInterval.value = setInterval(() => {
handleFetchPlugins();
}, 3000);
}
} catch (e) {
console.error("Failed to fetch plugins", e);
} finally {
@ -57,6 +71,10 @@ const handleFetchPlugins = async () => {
}
};
onBeforeRouteLeave(() => {
clearInterval(refreshInterval.value);
});
const handlePaginationChange = ({
page,
size,

View File

@ -69,6 +69,11 @@ const { isStarted, changeStatus, uninstall } = usePluginLifeCycle(plugin);
/>
</template>
</VEntityField>
<VEntityField v-if="plugin?.metadata.deletionTimestamp">
<template #description>
<VStatusDot v-tooltip="``" state="warning" animate />
</template>
</VEntityField>
<VEntityField>
<template #description>
<a