Revert "fix: harden automatic mobile boards"
This reverts commit d0b9348b86.
This commit is contained in:
1 parent
92a2be2b65
commit
dba9a250ff
37 files changed
+137
-637
No files matched your search
@@ -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"]);
|
||||
});
|
||||
});
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
Reference in new issue
Block a user