feat(FN-690): add event toggles to Settings modal

This commit is contained in:
gsxdsm
2026-04-02 10:35:12 -07:00
parent 144ce18d1d
commit 5d7a56de71
2 changed files with 164 additions and 0 deletions

View File

@@ -1363,6 +1363,7 @@ export function SettingsModal({
<small>Receive push notifications when tasks complete or fail via ntfy.sh</small>
</div>
{form.ntfyEnabled && (
<>
<div className="form-group">
<label htmlFor="ntfyTopic">ntfy Topic</label>
<input
@@ -1399,6 +1400,59 @@ export function SettingsModal({
{testNotificationLoading ? "Sending…" : "Test notification"}
</button>
</div>
<div className="form-group">
<label>Notify on events</label>
<div className="ntfy-events-list">
<label className="checkbox-label">
<input
type="checkbox"
checked={form.ntfyEvents?.includes("in-review") ?? true}
onChange={(e) => {
const current = form.ntfyEvents ?? ["in-review", "merged", "failed"];
const newEvents = e.target.checked
? [...new Set([...current, "in-review"])]
: current.filter((ev) => ev !== "in-review");
setForm((f) => ({ ...f, ntfyEvents: newEvents.length > 0 ? newEvents : undefined }));
}}
/>
Task completed (in-review)
</label>
<small>When a task moves to In Review (ready for review)</small>
<label className="checkbox-label">
<input
type="checkbox"
checked={form.ntfyEvents?.includes("merged") ?? true}
onChange={(e) => {
const current = form.ntfyEvents ?? ["in-review", "merged", "failed"];
const newEvents = e.target.checked
? [...new Set([...current, "merged"])]
: current.filter((ev) => ev !== "merged");
setForm((f) => ({ ...f, ntfyEvents: newEvents.length > 0 ? newEvents : undefined }));
}}
/>
Task merged
</label>
<small>When a task is successfully merged to main</small>
<label className="checkbox-label">
<input
type="checkbox"
checked={form.ntfyEvents?.includes("failed") ?? true}
onChange={(e) => {
const current = form.ntfyEvents ?? ["in-review", "merged", "failed"];
const newEvents = e.target.checked
? [...new Set([...current, "failed"])]
: current.filter((ev) => ev !== "failed");
setForm((f) => ({ ...f, ntfyEvents: newEvents.length > 0 ? newEvents : undefined }));
}}
/>
Task failed
</label>
<small>When a task fails during execution (high priority)</small>
</div>
</div>
</>
)}
</>
);

View File

@@ -22,6 +22,7 @@ const defaultSettings: Settings = {
defaultPresetBySize: {},
ntfyEnabled: false,
ntfyTopic: undefined,
ntfyEvents: ["in-review", "merged", "failed"],
taskStuckTimeoutMs: undefined,
};
@@ -1219,6 +1220,115 @@ describe("SettingsModal", () => {
expect(payload.ntfyEnabled).toBe(false);
});
it("shows ntfyEvents checkboxes when ntfy is enabled", async () => {
(fetchSettings as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
...defaultSettings,
ntfyEnabled: true,
ntfyTopic: "my-topic",
ntfyEvents: ["in-review", "merged", "failed"],
});
render(<SettingsModal onClose={onClose} addToast={addToast} />);
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
fireEvent.click(screen.getByText("Notifications"));
await waitFor(() => expect(screen.getByLabelText("Task completed (in-review)")).toBeTruthy());
expect(screen.getByLabelText("Task merged")).toBeTruthy();
expect(screen.getByLabelText("Task failed")).toBeTruthy();
});
it("hides ntfyEvents checkboxes when ntfy is disabled", async () => {
render(<SettingsModal onClose={onClose} addToast={addToast} />);
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
fireEvent.click(screen.getByText("Notifications"));
expect(screen.queryByLabelText("Task completed (in-review)")).toBeNull();
expect(screen.queryByLabelText("Task merged")).toBeNull();
expect(screen.queryByLabelText("Task failed")).toBeNull();
});
it("ntfyEvents checkboxes are all checked by default", async () => {
(fetchSettings as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
...defaultSettings,
ntfyEnabled: true,
ntfyTopic: "my-topic",
});
render(<SettingsModal onClose={onClose} addToast={addToast} />);
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
fireEvent.click(screen.getByText("Notifications"));
expect((screen.getByLabelText("Task completed (in-review)") as HTMLInputElement).checked).toBe(true);
expect((screen.getByLabelText("Task merged") as HTMLInputElement).checked).toBe(true);
expect((screen.getByLabelText("Task failed") as HTMLInputElement).checked).toBe(true);
});
it("saves ntfyEvents correctly when checkboxes are toggled", async () => {
(fetchSettings as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
...defaultSettings,
ntfyEnabled: true,
ntfyTopic: "my-topic",
ntfyEvents: ["in-review", "merged", "failed"],
});
render(<SettingsModal onClose={onClose} addToast={addToast} />);
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
fireEvent.click(screen.getByText("Notifications"));
// Uncheck "Task merged"
const mergedCheckbox = screen.getByLabelText("Task merged");
fireEvent.click(mergedCheckbox);
fireEvent.click(screen.getByText("Save"));
await waitFor(() => expect(updateGlobalSettings).toHaveBeenCalledTimes(1));
const payload = (updateGlobalSettings as ReturnType<typeof vi.fn>).mock.calls[0][0];
expect(payload.ntfyEvents).toEqual(["in-review", "failed"]);
});
it("sets ntfyEvents to undefined when all checkboxes are unchecked", async () => {
(fetchSettings as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
...defaultSettings,
ntfyEnabled: true,
ntfyTopic: "my-topic",
ntfyEvents: ["in-review", "merged", "failed"],
});
render(<SettingsModal onClose={onClose} addToast={addToast} />);
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
fireEvent.click(screen.getByText("Notifications"));
// Uncheck all three
fireEvent.click(screen.getByLabelText("Task completed (in-review)"));
fireEvent.click(screen.getByLabelText("Task merged"));
fireEvent.click(screen.getByLabelText("Task failed"));
fireEvent.click(screen.getByText("Save"));
await waitFor(() => expect(updateGlobalSettings).toHaveBeenCalledTimes(1));
const payload = (updateGlobalSettings as ReturnType<typeof vi.fn>).mock.calls[0][0];
expect(payload.ntfyEvents).toBeUndefined();
});
it("restores ntfyEvents from saved settings", async () => {
(fetchSettings as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
...defaultSettings,
ntfyEnabled: true,
ntfyTopic: "my-topic",
ntfyEvents: ["in-review"],
});
render(<SettingsModal onClose={onClose} addToast={addToast} />);
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
fireEvent.click(screen.getByText("Notifications"));
expect((screen.getByLabelText("Task completed (in-review)") as HTMLInputElement).checked).toBe(true);
expect((screen.getByLabelText("Task merged") as HTMLInputElement).checked).toBe(false);
expect((screen.getByLabelText("Task failed") as HTMLInputElement).checked).toBe(false);
});
// Model filter tests with CustomModelDropdown
it("renders filter input in Default Model section dropdown", async () => {
const user = userEvent.setup();