diff --git a/apps/nextjs/src/components/board/items/widget-context-menu.tsx b/apps/nextjs/src/components/board/items/widget-context-menu.tsx index 415151a52..c3f3469cb 100644 --- a/apps/nextjs/src/components/board/items/widget-context-menu.tsx +++ b/apps/nextjs/src/components/board/items/widget-context-menu.tsx @@ -2,10 +2,10 @@ import type { MutableRefObject, ReactNode } from "react"; import { useCallback, useEffect, useMemo, useState } from "react"; -import { Group, Menu, Switch, Text } from "@mantine/core"; +import { Group, Loader, Menu, Switch, Text } from "@mantine/core"; import { IconCopy, IconLayoutKanban, IconRefresh, IconSettings, IconTrash } from "@tabler/icons-react"; import type { QueryClient } from "@tanstack/react-query"; -import { useQueryClient } from "@tanstack/react-query"; +import { useIsFetching, useQueryClient } from "@tanstack/react-query"; import { clientApi } from "@homarr/api/client"; import { useSession } from "@homarr/auth/client"; @@ -50,8 +50,9 @@ export const WidgetContextMenu = ({ item, widgetStateRef, children }: WidgetCont const queryClient = useQueryClient(); const widgetQueryKey = useMemo(() => [["widget", item.kind]], [item.kind]); + const isWidgetFetching = useIsFetching({ queryKey: widgetQueryKey }) > 0; const handleRefetch = useCallback(() => { - void queryClient.invalidateQueries({ queryKey: widgetQueryKey }); + void queryClient.refetchQueries({ queryKey: widgetQueryKey, type: "all" }); }, [queryClient, widgetQueryKey]); const options = useMemo( @@ -223,11 +224,15 @@ export const WidgetContextMenu = ({ item, widgetStateRef, children }: WidgetCont <> {(toggleOptions.length > 0 || visibleWidgetActions.length > 0 || isEditMode) && } - } onClick={handleRefetch}> + : } + onClick={handleRefetch} + disabled={isWidgetFetching} + > {tMenu("refresh")} - + @@ -265,13 +270,24 @@ export const WidgetContextMenu = ({ item, widgetStateRef, children }: WidgetCont ); }; -const WidgetCacheAge = ({ queryClient, queryKey }: { queryClient: QueryClient; queryKey: unknown[] }) => { +const WidgetCacheAge = ({ + queryClient, + queryKey, + isFetching, +}: { + queryClient: QueryClient; + queryKey: unknown[]; + isFetching: boolean; +}) => { + // 1s tick for "just now" → "1s ago" label transition; isFetching prop handles refetch reactivity const [, setTick] = useState(0); useEffect(() => { const id = setInterval(() => setTick((n) => n + 1), 1000); return () => clearInterval(id); }, []); + if (isFetching) return ; + const timestamps = queryClient .getQueryCache() .findAll({ queryKey })