Chore: full react hooks eslint compliance (#7040)

This commit is contained in:
shamoon
2026-08-21 14:00:02 -07:00
committed by GitHub
parent 7c26cf6d97
commit 4c361422ca
111 changed files with 1072 additions and 867 deletions
+4 -4
View File
@@ -38,16 +38,16 @@ const colorVariants = {
export default function Component({ service }) {
const { widget } = service;
const { i18n } = useTranslation();
const [showDate, setShowDate] = useState(null);
const [events, setEvents] = useState({});
const nowDate = DateTime.now().setLocale(i18n.language);
const currentDate = widget?.timezone ? nowDate.setZone(widget?.timezone).startOf("day") : nowDate;
const [showDate, setShowDate] = useState(null);
const { settings } = useContext(SettingsContext);
useEffect(() => {
if (!showDate) {
setShowDate(currentDate);
}
// seeded after mount, not during render: "today" is client-only and would break hydration
// eslint-disable-next-line react-hooks/set-state-in-effect
if (!showDate) setShowDate(currentDate);
}, [showDate, currentDate]);
// params for API fetch
+53 -51
View File
@@ -1,7 +1,7 @@
import ICAL from "ical.js";
import { DateTime } from "luxon";
import { useTranslation } from "next-i18next/pages";
import { useEffect } from "react";
import { useEffect, useMemo } from "react";
import Error from "../../../components/services/widget/error";
import useWidgetAPI from "../../../utils/proxy/use-widget-api";
@@ -17,65 +17,67 @@ function simpleHash(str) {
return Math.abs(hash).toString(36);
}
function buildEvent(event, type) {
return {
id: event.getFirstPropertyValue("uid"),
type,
title: event.getFirstPropertyValue("summary"),
rrule: event.getFirstPropertyValue("rrule"),
dtstart:
event.getFirstPropertyValue("dtstart") ||
event.getFirstPropertyValue("due") ||
event.getFirstPropertyValue("completed") ||
ICAL.Time.now(), // handles events without a date
dtend:
event.getFirstPropertyValue("dtend") ||
event.getFirstPropertyValue("due") ||
event.getFirstPropertyValue("completed") ||
ICAL.Time.now(), // handles events without a date
location: event.getFirstPropertyValue("location"),
status: event.getFirstPropertyValue("status"),
url: event.getFirstPropertyValue("url"),
};
}
export default function Integration({ config, params, setEvents, hideErrors, timezone }) {
const { t } = useTranslation();
const { data: icalData, error: icalError } = useWidgetAPI(config, config.name, {
refreshInterval: 300000, // 5 minutes
});
const { events, dataError } = useMemo(() => {
if (icalError || !icalData || icalData.error) {
return { events: [], dataError: undefined };
}
if (!icalData.data) {
return {
events: [],
dataError: { message: `'${config.name}': ${t("calendar.errorWhenLoadingData")}` },
};
}
const jCal = ICAL.parse(icalData.data);
const vCalendar = new ICAL.Component(jCal);
const parsedEvents = [
...vCalendar.getAllSubcomponents("vevent").map((event) => buildEvent(event, "vevent")),
...vCalendar.getAllSubcomponents("vtodo").map((todo) => buildEvent(todo, "vtodo")),
];
return {
events: parsedEvents,
dataError:
parsedEvents.length === 0 ? { message: `'${config.name}': ${t("calendar.noEventsFound")}` } : undefined,
};
}, [icalData, icalError, config.name, t]);
useEffect(() => {
const { showName = false } = config?.params || {};
let events = [];
if (!icalError && icalData && !icalData.error) {
if (!icalData.data) {
icalData.error = { message: `'${config.name}': ${t("calendar.errorWhenLoadingData")}` };
return;
}
const jCal = ICAL.parse(icalData.data);
const vCalendar = new ICAL.Component(jCal);
const buildEvent = (event, type) => {
return {
id: event.getFirstPropertyValue("uid"),
type,
title: event.getFirstPropertyValue("summary"),
rrule: event.getFirstPropertyValue("rrule"),
dtstart:
event.getFirstPropertyValue("dtstart") ||
event.getFirstPropertyValue("due") ||
event.getFirstPropertyValue("completed") ||
ICAL.Time.now(), // handles events without a date
dtend:
event.getFirstPropertyValue("dtend") ||
event.getFirstPropertyValue("due") ||
event.getFirstPropertyValue("completed") ||
ICAL.Time.now(), // handles events without a date
location: event.getFirstPropertyValue("location"),
status: event.getFirstPropertyValue("status"),
url: event.getFirstPropertyValue("url"),
};
};
const getEvents = () => {
const vEvents = vCalendar.getAllSubcomponents("vevent").map((event) => buildEvent(event, "vevent"));
const vTodos = vCalendar.getAllSubcomponents("vtodo").map((todo) => buildEvent(todo, "vtodo"));
return [...vEvents, ...vTodos];
};
events = getEvents();
if (events.length === 0) {
icalData.error = { message: `'${config.name}': ${t("calendar.noEventsFound")}` };
}
}
const startDate = DateTime.fromISO(params.start);
const endDate = DateTime.fromISO(params.end);
if (icalError || events.length === 0 || !startDate.isValid || !endDate.isValid) {
if (events.length === 0 || !startDate.isValid || !endDate.isValid) {
return;
}
@@ -113,7 +115,7 @@ export default function Integration({ config, params, setEvents, hideErrors, tim
return occurrences;
};
const eventsToAdd = [];
const eventsToAdd = {};
events.forEach((event) => {
const occurrences = getOcurrencesFromRange(event);
@@ -154,8 +156,8 @@ export default function Integration({ config, params, setEvents, hideErrors, tim
});
setEvents((prevEvents) => ({ ...prevEvents, ...eventsToAdd }));
}, [icalData, icalError, config, params, setEvents, timezone, t]);
}, [events, config, params, setEvents, timezone]);
const error = icalError ?? icalData?.error;
const error = icalError ?? icalData?.error ?? dataError;
return error && !hideErrors && <Error error={{ message: `${config.type}: ${error.message ?? error}` }} />;
}
@@ -1,6 +1,6 @@
// @vitest-environment jsdom
import { render, waitFor } from "@testing-library/react";
import { render, screen, waitFor } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
const { useWidgetAPI } = vi.hoisted(() => ({
@@ -12,6 +12,45 @@ vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
import Integration from "./ical";
describe("widgets/calendar/integrations/ical", () => {
it("reports a missing calendar payload without mutating the response", () => {
const data = {};
useWidgetAPI.mockReturnValue({ data, error: undefined });
render(
<Integration
config={{ name: "Work", type: "ical" }}
params={{ start: "2099-01-01", end: "2099-01-02" }}
setEvents={vi.fn()}
hideErrors={false}
timezone="utc"
/>,
);
expect(screen.getByText(/'Work': calendar\.errorWhenLoadingData/)).toBeInTheDocument();
expect(data).toEqual({});
});
it("reports a calendar with no events without mutating the response", () => {
const data = {
data: ["BEGIN:VCALENDAR", "VERSION:2.0", "PRODID:-//Test//EN", "END:VCALENDAR", ""].join("\n"),
};
const originalData = structuredClone(data);
useWidgetAPI.mockReturnValue({ data, error: undefined });
render(
<Integration
config={{ name: "Empty", type: "ical" }}
params={{ start: "2099-01-01", end: "2099-01-02" }}
setEvents={vi.fn()}
hideErrors={false}
timezone="utc"
/>,
);
expect(screen.getByText(/'Empty': calendar\.noEventsFound/)).toBeInTheDocument();
expect(data).toEqual(originalData);
});
it("adds parsed events within the date range", async () => {
useWidgetAPI.mockReturnValue({
data: {