feat(FN-690): add event toggles to Settings modal
This commit is contained in:
@@ -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>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user