intermittent commit
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Mail, Save } from "lucide-react";
|
||||
import { Button, Card, DismissibleAlert, FormField, ToggleSwitch, type ApiSettings, type AuthInfo } from "@govoplan/core-webui";
|
||||
import { getNotificationPreferences, updateNotificationPreferences, type NotificationPreferences } from "../../api/notifications";
|
||||
|
||||
type Draft = Pick<NotificationPreferences, "show_unread_badge" | "email_enabled" | "email_digest_enabled"> & {
|
||||
muted_source_modules_text: string;
|
||||
};
|
||||
|
||||
const DEFAULT_DRAFT: Draft = {
|
||||
show_unread_badge: true,
|
||||
email_enabled: false,
|
||||
email_digest_enabled: false,
|
||||
muted_source_modules_text: ""
|
||||
};
|
||||
|
||||
export default function NotificationSettingsPanel({ settings, auth }: { settings: ApiSettings; auth: AuthInfo }) {
|
||||
const [loaded, setLoaded] = useState<NotificationPreferences | null>(null);
|
||||
const [draft, setDraft] = useState<Draft>(DEFAULT_DRAFT);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [message, setMessage] = useState("");
|
||||
const [messageTone, setMessageTone] = useState<"success" | "warning">("success");
|
||||
|
||||
const dirty = useMemo(() => {
|
||||
if (!loaded) return false;
|
||||
return (
|
||||
draft.show_unread_badge !== loaded.show_unread_badge ||
|
||||
draft.email_enabled !== loaded.email_enabled ||
|
||||
draft.email_digest_enabled !== loaded.email_digest_enabled ||
|
||||
normalizeMutedModules(draft.muted_source_modules_text).join(",") !== loaded.muted_source_modules.join(",")
|
||||
);
|
||||
}, [draft, loaded]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadPreferences();
|
||||
}, [auth.user.id, settings.accessToken, settings.apiBaseUrl, settings.apiKey]);
|
||||
|
||||
async function loadPreferences() {
|
||||
setLoading(true);
|
||||
setMessage("");
|
||||
try {
|
||||
const preferences = await getNotificationPreferences(settings);
|
||||
setLoaded(preferences);
|
||||
setDraft({
|
||||
show_unread_badge: preferences.show_unread_badge,
|
||||
email_enabled: preferences.email_enabled,
|
||||
email_digest_enabled: preferences.email_digest_enabled,
|
||||
muted_source_modules_text: preferences.muted_source_modules.join(", ")
|
||||
});
|
||||
} catch (error) {
|
||||
setMessageTone("warning");
|
||||
setMessage(error instanceof Error ? error.message : "Loading notification preferences failed");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function savePreferences() {
|
||||
setSaving(true);
|
||||
setMessage("");
|
||||
try {
|
||||
const next = await updateNotificationPreferences(settings, {
|
||||
show_unread_badge: draft.show_unread_badge,
|
||||
email_enabled: draft.email_enabled,
|
||||
email_digest_enabled: draft.email_digest_enabled,
|
||||
muted_source_modules: normalizeMutedModules(draft.muted_source_modules_text)
|
||||
});
|
||||
setLoaded(next);
|
||||
setDraft({
|
||||
show_unread_badge: next.show_unread_badge,
|
||||
email_enabled: next.email_enabled,
|
||||
email_digest_enabled: next.email_digest_enabled,
|
||||
muted_source_modules_text: next.muted_source_modules.join(", ")
|
||||
});
|
||||
window.dispatchEvent(new CustomEvent("govoplan:notifications-changed"));
|
||||
setMessageTone("success");
|
||||
setMessage("Notification preferences saved.");
|
||||
} catch (error) {
|
||||
setMessageTone("warning");
|
||||
setMessage(error instanceof Error ? error.message : "Saving notification preferences failed");
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="dashboard-grid settings-dashboard-grid notifications-settings-panel">
|
||||
<Card title="Notifications">
|
||||
<div className="form-grid">
|
||||
<ToggleSwitch
|
||||
label="Unread badge"
|
||||
help="Show unread notification counts in the top bar."
|
||||
checked={draft.show_unread_badge}
|
||||
disabled={loading}
|
||||
onChange={(value) => setDraft((current) => ({ ...current, show_unread_badge: value }))}
|
||||
/>
|
||||
<FormField label="Muted source modules" help="Comma-separated module IDs. Filtering will be applied by later delivery rules.">
|
||||
<input
|
||||
value={draft.muted_source_modules_text}
|
||||
onChange={(event) => setDraft((current) => ({ ...current, muted_source_modules_text: event.target.value }))}
|
||||
placeholder="calendar, campaign"
|
||||
disabled={loading}
|
||||
/>
|
||||
</FormField>
|
||||
<div className="button-row compact-actions">
|
||||
<Button variant="primary" onClick={() => void savePreferences()} disabled={loading || saving || !dirty}>
|
||||
<Save size={16} /> {saving ? "Saving" : "Save preferences"}
|
||||
</Button>
|
||||
</div>
|
||||
{message ? <DismissibleAlert tone={messageTone} resetKey={message} floating>{message}</DismissibleAlert> : null}
|
||||
</div>
|
||||
</Card>
|
||||
<Card title="Delivery">
|
||||
<div className="form-grid">
|
||||
<div className="notifications-settings-inline-title">
|
||||
<Mail size={16} />
|
||||
<strong>Email notifications</strong>
|
||||
</div>
|
||||
<ToggleSwitch
|
||||
label="Email notifications"
|
||||
help="Prepared for mail-backed notification delivery."
|
||||
checked={draft.email_enabled}
|
||||
disabled={loading}
|
||||
onChange={(value) => setDraft((current) => ({ ...current, email_enabled: value, email_digest_enabled: value ? current.email_digest_enabled : false }))}
|
||||
/>
|
||||
<ToggleSwitch
|
||||
label="Email digest"
|
||||
help="Batch eligible notifications into summary messages when delivery rules support it."
|
||||
checked={draft.email_digest_enabled}
|
||||
disabled={loading || !draft.email_enabled}
|
||||
onChange={(value) => setDraft((current) => ({ ...current, email_digest_enabled: value }))}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeMutedModules(value: string): string[] {
|
||||
const seen = new Set<string>();
|
||||
return value
|
||||
.split(",")
|
||||
.map((item) => item.trim().toLowerCase())
|
||||
.filter((item) => {
|
||||
if (!item || seen.has(item)) return false;
|
||||
seen.add(item);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user