fix: mutation error notifications, query error state, ratio guards, memoization, composite ids, docs accuracy

This commit is contained in:
Thomas Camlong committed 2026-07-21 16:05:59 +02:00
1 parent 5acf6cef32
commit 5c71c1aede
3 files changed
+33 -25

No files matched your search

+1 -2
View File
@@ -44,7 +44,7 @@ import contextMenuImg from './img/context-menu.png';
- **Responsive table** — columns automatically adapt to widget width, hiding less important columns at smaller sizes
- **Sortable columns** — click any column header to sort ascending/descending
- **Column customization** — toggle, reorder, resize, and pin columns via the header menu; preferences persist in local storage
- **Column customization** — choose which columns to display via the widget options panel
- **Right-click context menu** — pause, resume, delete downloads, or copy download names directly from the context menu
- **Row expansion** — click any row to see full details inline without opening a separate modal
- **Hover tooltips** — hover a download name to see size, speeds, ETA, ratio, and category
@@ -84,7 +84,6 @@ import contextMenuImg from './img/context-menu.png';
- **Right-click** any download to open a context menu with pause/resume, delete, and copy actions
- **Click** any row to expand it and see full download details inline
- **Hover** a download name for a quick stats tooltip
- **Column header menu** — show/hide, reorder, resize, or pin columns (reset via the columns button in the footer)
- **Stats toggle** — show or hide the global stats bar from the footer
- **Pause / Resume** all downloads using the footer controls
- **Filter** by integration or status using the filter button in the footer
+4 -9
View File
@@ -3073,7 +3073,10 @@
},
"errors": {
"noColumns": "Select Columns in Items",
"noCommunications": "Can't load data from integration"
"noCommunications": "Can't load data from integration",
"noItems": "No downloads found",
"actionFailed": "Action failed",
"actionFailedMessage": "The action could not be completed. Please try again."
},
"items": {
"actions": {
@@ -3189,14 +3192,6 @@
"overallProgress": "Overall queue progress",
"totalSize": "Total queue size",
"clients": "{count, plural, one {# client} other {# clients}}"
},
"columns": {
"settings": "Column settings",
"resetVisibility": "Reset visibility",
"resetOrder": "Reset order",
"resetWidth": "Reset widths",
"resetPinning": "Reset pinning",
"scrolled": "Scrolled {y}px from top"
}
},
"mediaReleases": {
+28 -14
View File
@@ -48,6 +48,7 @@ import { useIntegrationsWithInteractAccess } from "@homarr/auth/client";
import { formatByteRate, formatBytes, useIntegrationConnected } from "@homarr/common";
import { getIconUrl, getIntegrationKindsByCategory } from "@homarr/definitions";
import type { ExtendedClientStatus, ExtendedDownloadClientItem } from "@homarr/integrations";
import { showErrorNotification } from "@homarr/notifications";
import { useScopedI18n } from "@homarr/translation/client";
import type { WidgetComponentProps } from "../definition";
@@ -117,11 +118,13 @@ export default function DownloadClientsWidget({
options,
width,
}: WidgetComponentProps<"downloads">) {
const integrationsWithInteractions = useIntegrationsWithInteractAccess().flatMap(({ id }) =>
integrationIds.includes(id) ? [id] : [],
const allInteractAccess = useIntegrationsWithInteractAccess();
const integrationsWithInteractions = useMemo(
() => allInteractAccess.flatMap(({ id }) => (integrationIds.includes(id) ? [id] : [])),
[allInteractAccess, integrationIds],
);
const { data: currentItems = [], isFetching } = clientApi.widget.downloads.getJobsAndStatuses.useQuery({
const { data: currentItems = [], isFetching, isError } = clientApi.widget.downloads.getJobsAndStatuses.useQuery({
integrationIds,
limitPerIntegration: options.limitPerIntegration,
});
@@ -134,12 +137,15 @@ export default function DownloadClientsWidget({
const [showStats, { toggle: toggleStats }] = useDisclosure(false);
const utils = clientApi.useUtils();
const invalidateDownloads = { onSettled: () => void utils.widget.downloads.getJobsAndStatuses.invalidate() };
const { mutate: mutateResumeItem } = clientApi.widget.downloads.resumeItem.useMutation(invalidateDownloads);
const { mutate: mutatePauseItem } = clientApi.widget.downloads.pauseItem.useMutation(invalidateDownloads);
const { mutate: mutateDeleteItem } = clientApi.widget.downloads.deleteItem.useMutation(invalidateDownloads);
const { mutate: mutateResumeQueue } = clientApi.widget.downloads.resume.useMutation(invalidateDownloads);
const { mutate: mutatePauseQueue } = clientApi.widget.downloads.pause.useMutation(invalidateDownloads);
const mutationOptions = {
onSettled: () => void utils.widget.downloads.getJobsAndStatuses.invalidate(),
onError: () => showErrorNotification({ title: t("errors.actionFailed"), message: t("errors.actionFailedMessage") }),
};
const { mutate: mutateResumeItem } = clientApi.widget.downloads.resumeItem.useMutation(mutationOptions);
const { mutate: mutatePauseItem } = clientApi.widget.downloads.pauseItem.useMutation(mutationOptions);
const { mutate: mutateDeleteItem } = clientApi.widget.downloads.deleteItem.useMutation(mutationOptions);
const { mutate: mutateResumeQueue } = clientApi.widget.downloads.resume.useMutation(mutationOptions);
const { mutate: mutatePauseQueue } = clientApi.widget.downloads.pause.useMutation(mutationOptions);
const [sortStatus, setSortStatus] = useState<DataTableSortStatus<ExtendedDownloadClientItem>>({
columnAccessor: options.defaultSort,
@@ -176,7 +182,7 @@ export default function DownloadClientsWidget({
...item,
category: item.category !== undefined && item.category.length > 0 ? item.category : undefined,
received,
ratio: item.sent !== undefined ? item.sent / item.size : undefined,
ratio: item.sent !== undefined && item.size > 0 ? item.sent / item.size : undefined,
actions: integrationsWithInteractions.includes(pair.integration.id)
? {
resume: () => mutateResumeItem({ integrationIds: ids, item }),
@@ -235,7 +241,7 @@ export default function DownloadClientsWidget({
return {
integration,
interact,
status: { totalUp, totalDown, ratio: totalUp === undefined ? undefined : totalUp / totalDown, ...d.status },
status: { totalUp, totalDown, ratio: totalUp === undefined || totalDown === 0 ? undefined : totalUp / totalDown, ...d.status },
};
});
}, [currentItems, integrationIds, integrationsWithInteractions, options.applyFilterToRatio, options.categoryFilter, options.filterIsWhitelist]);
@@ -244,7 +250,7 @@ export default function DownloadClientsWidget({
const hasMultipleClients = clients.length > 1;
const hasMultipleTypes = new Set(data.map(({ type }) => type)).size > 1;
const size = getSizeConfig(width);
const size = useMemo(() => getSizeConfig(width), [width]);
const optionsColumnSet = useMemo(() => new Set<string>(options.columns), [options.columns]);
const shouldIncludeColumn = useCallback(
@@ -489,6 +495,14 @@ export default function DownloadClientsWidget({
);
}
if (isError && currentItems.length === 0) {
return (
<Center h="100%">
<Text c="red">{t("errors.noCommunications")}</Text>
</Center>
);
}
return (
<Stack gap={0} h="100%" style={{ overflow: "hidden" }}>
{showStats && (
@@ -520,8 +534,8 @@ export default function DownloadClientsWidget({
textSelectionDisabled
sortStatus={sortStatus}
onSortStatusChange={setSortStatus}
noRecordsText={t("errors.noColumns")}
idAccessor="id"
noRecordsText={t("errors.noItems")}
idAccessor={(record) => `${record.integration.id}:${record.id}`}
height="100%"
className="downloads-table"
defaultColumnProps={{