fix: mutation error notifications, query error state, ratio guards, memoization, composite ids, docs accuracy
This commit is contained in:
3 files changed
+33
-25
No files matched your search
@@ -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
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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={{
|
||||
|
||||
Reference in new issue
Block a user