Revert "fix: harden automatic mobile boards"

This reverts commit d0b9348b86.
This commit is contained in:
ajnart committed 2026-07-29 20:06:32 +02:00
1 parent 92a2be2b65
commit dba9a250ff
37 files changed
+137 -637

No files matched your search

-1
View File
@@ -75,7 +75,6 @@
"@tiptap/starter-kit": "catalog:",
"clsx": "catalog:",
"dayjs": "catalog:",
"isomorphic-dompurify": "catalog:",
"jsonpath-plus": "catalog:",
"lucide-react": "catalog:",
"mantine-datatable": "catalog:",
@@ -1,14 +1,6 @@
import { MantineProvider } from "@mantine/core";
import { createElement } from "react";
import type { ComponentProps } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, test, vi } from "vitest";
import { describe, expect, test } from "vitest";
import { hasWidgetDataWarning, throwOnInitialQueryError, WidgetDataState } from "./query-state";
vi.mock("@homarr/translation/client", () => ({
useI18n: () => () => "Some widget data may be stale",
}));
import { hasWidgetDataWarning, throwOnInitialQueryError } from "./query-state";
describe("widget query state", () => {
test.each([
@@ -39,22 +31,4 @@ describe("widget query state", () => {
test("keeps cached data visible after a refresh error", () => {
expect(() => throwOnInitialQueryError(new Error("refresh failed"), true)).not.toThrow();
});
test("places the warning after widget controls in keyboard focus order", () => {
const markup = renderToStaticMarkup(
createElement(
MantineProvider,
null,
createElement(
WidgetDataState,
{ hasWarning: true } as ComponentProps<typeof WidgetDataState>,
createElement("button", { type: "button" }, "Widget action"),
),
),
);
const document = new DOMParser().parseFromString(markup, "text/html");
const focusableElements = [...document.querySelectorAll("button, [tabindex='0']")];
expect(focusableElements.map((element) => element.tagName)).toStrictEqual(["BUTTON", "OUTPUT"]);
});
});
+2 -3
View File
@@ -43,13 +43,11 @@ export function WidgetDataState({ children, hasWarning }: WidgetDataStateProps)
return (
<Box pos="relative" h="100%" w="100%">
{children}
{hasWarning && (
<Tooltip label={label} position="left" withArrow events={{ hover: true, focus: true, touch: true }}>
<Tooltip label={label} position="left" withArrow>
<Center
component="output"
aria-label={label}
tabIndex={0}
pos="absolute"
bottom={4}
w={28}
@@ -69,6 +67,7 @@ export function WidgetDataState({ children, hasWarning }: WidgetDataStateProps)
</Center>
</Tooltip>
)}
{children}
</Box>
);
}
@@ -24,7 +24,6 @@ import { useScopedI18n } from "@homarr/translation/client";
import { WidgetEmptyState } from "../common/empty-state";
import { WidgetMobileLoading, WidgetMobileSummary } from "../common/mobile-summary";
import { hasWidgetDataWarning, WidgetDataState } from "../common/query-state";
import type { WidgetComponentProps } from "../definition";
import { NoIntegrationDataError } from "../errors/no-data-integration";
import classes from "./component.module.css";
@@ -60,11 +59,6 @@ export default function MediaMissingWidget({
const queued = data.flatMap((entry) => entry.queued.map((item) => ({ item, integrationId: entry.integrationId })));
const missingCount = data.reduce((sum, entry) => sum + entry.missingCount, 0);
const queuedCount = data.reduce((sum, entry) => sum + entry.queuedCount, 0);
const hasDataWarning = hasWidgetDataWarning({
error,
expectedIntegrationCount: integrationIds.length,
receivedIntegrationCount: data.length,
});
if (displayMode === "mobileSummary") {
const primary = options.showMissing
@@ -77,7 +71,7 @@ export default function MediaMissingWidget({
value={primary.value}
label={primary.label}
description={description}
isStale={hasDataWarning}
isStale={Boolean(error)}
/>
);
}
@@ -112,37 +106,35 @@ export default function MediaMissingWidget({
);
return (
<WidgetDataState hasWarning={hasDataWarning}>
<Tabs
defaultValue={options.showMissing ? "missing" : "queued"}
h="100%"
style={{ display: "flex", flexDirection: "column" }}
>
<Tabs.List grow>
{options.showMissing && (
<Tabs.Tab value="missing" px={isThin ? 6 : undefined} leftSection={<IconQuestionMark size={14} />}>
{tabLabel(t("tab.missing"), missing.length, missingCount)}
</Tabs.Tab>
)}
{options.showQueued && (
<Tabs.Tab value="queued" px={isThin ? 6 : undefined} leftSection={<IconDownload size={14} />}>
{tabLabel(t("tab.queued"), queued.length, queuedCount)}
</Tabs.Tab>
)}
</Tabs.List>
<Tabs
defaultValue={options.showMissing ? "missing" : "queued"}
h="100%"
style={{ display: "flex", flexDirection: "column" }}
>
<Tabs.List grow>
{options.showMissing && (
<Tabs.Panel value="missing" flex={1} style={{ overflow: "hidden" }}>
{renderPanel(missing, t("empty.missing"))}
</Tabs.Panel>
<Tabs.Tab value="missing" px={isThin ? 6 : undefined} leftSection={<IconQuestionMark size={14} />}>
{tabLabel(t("tab.missing"), missing.length, missingCount)}
</Tabs.Tab>
)}
{options.showQueued && (
<Tabs.Panel value="queued" flex={1} style={{ overflow: "hidden" }}>
{renderPanel(queued, t("empty.queued"))}
</Tabs.Panel>
<Tabs.Tab value="queued" px={isThin ? 6 : undefined} leftSection={<IconDownload size={14} />}>
{tabLabel(t("tab.queued"), queued.length, queuedCount)}
</Tabs.Tab>
)}
</Tabs>
</WidgetDataState>
</Tabs.List>
{options.showMissing && (
<Tabs.Panel value="missing" flex={1} style={{ overflow: "hidden" }}>
{renderPanel(missing, t("empty.missing"))}
</Tabs.Panel>
)}
{options.showQueued && (
<Tabs.Panel value="queued" flex={1} style={{ overflow: "hidden" }}>
{renderPanel(queued, t("empty.queued"))}
</Tabs.Panel>
)}
</Tabs>
);
}
@@ -112,7 +112,7 @@ export default function MediaServerWidget({
<WidgetMobileSummary
value={total}
label={t("titles.stats.total")}
description={t("titles.stats.pendingCount", { pending })}
description={`${pending} ${t("titles.stats.pending")}`}
isStale={hasWarning}
/>
);
+2 -2
View File
@@ -10,7 +10,6 @@ import { useRequiredBoard } from "@homarr/boards/context";
import type { WidgetComponentProps } from "../definition";
import classes from "./component.module.scss";
import { sanitizeFeedDescription } from "./sanitize-description";
const useLiveFeedEntries = (input: RouterInputs["widget"]["rssFeed"]["getFeeds"]) => {
const { data: feedEntries = [] } = clientApi.widget.rssFeed.getFeeds.useQuery(input);
@@ -57,11 +56,12 @@ export default function RssFeed({ options, width, displayMode }: WidgetComponent
</Text>
{!options.hideDescription && feedEntry.description && (
<Text
className={feedEntry.description}
dir={languageDir}
c="dimmed"
size="sm"
lineClamp={options.textLinesClamp}
dangerouslySetInnerHTML={{ __html: sanitizeFeedDescription(feedEntry.description) }}
dangerouslySetInnerHTML={{ __html: feedEntry.description }}
/>
)}
@@ -1,12 +0,0 @@
import { describe, expect, test } from "vitest";
import { sanitizeFeedDescription } from "./sanitize-description";
describe("sanitizeFeedDescription", () => {
test("keeps basic formatting while removing executable markup", () => {
const description =
'<p onclick="alert(1)" aria-hidden="true" data-tracking="yes">News <strong>today</strong><script>alert(2)</script><img src="tracker"></p>';
expect(sanitizeFeedDescription(description)).toBe("<p>News <strong>today</strong></p>");
});
});
@@ -1,11 +0,0 @@
import { sanitize } from "isomorphic-dompurify";
const allowedDescriptionTags = ["b", "br", "em", "i", "li", "ol", "p", "s", "strong", "u", "ul"];
export const sanitizeFeedDescription = (description: string) =>
sanitize(description, {
ALLOW_ARIA_ATTR: false,
ALLOW_DATA_ATTR: false,
ALLOWED_ATTR: [],
ALLOWED_TAGS: allowedDescriptionTags,
});