Files
RedditBot/apps/dashboard/src/features/settings/settings-editor.tsx
T

119 lines
4.9 KiB
TypeScript

import { useEffect, useState } from 'react';
import type { ConfigResponse, ConfigUpdateRequest } from '../../../../../packages/api-contracts/src/index.js';
import { AccessPanel } from './access-panel.js';
import { RiskDialog } from './risk-dialog.js';
import { useSaveSettingsMutation } from './settings-queries.js';
import { createSettingsDraft, isDraftDirty, validateSettingsDraft, type SettingsDraft } from './settings-model.js';
import { BehaviourSection, MonitoringSection, RateLimitsSection } from './settings-sections.js';
import type { SettingsSection } from './settings-page.js';
interface SettingsEditorProps {
config: ConfigResponse;
section: SettingsSection;
}
interface PendingRiskySave {
payload: ConfigUpdateRequest;
reasons: string[];
}
function errorMessage(error: Error | null): string {
return error?.message ?? 'Something went wrong.';
}
export function SettingsEditor({ config, section }: SettingsEditorProps) {
const [persistedDraft, setPersistedDraft] = useState<SettingsDraft>(() => createSettingsDraft(config));
const [draft, setDraft] = useState<SettingsDraft>(persistedDraft);
const [errors, setErrors] = useState<Record<string, string>>({});
const [pendingRiskySave, setPendingRiskySave] = useState<PendingRiskySave | null>(null);
const saveMutation = useSaveSettingsMutation();
const isDirty = isDraftDirty(draft, persistedDraft);
useEffect(() => {
function warnBeforeUnload(event: BeforeUnloadEvent) {
if (!isDirty) return;
event.preventDefault();
event.returnValue = '';
}
window.addEventListener('beforeunload', warnBeforeUnload);
return () => window.removeEventListener('beforeunload', warnBeforeUnload);
}, [isDirty]);
function applySavedResponse(response: Awaited<ReturnType<typeof saveMutation.mutateAsync>>) {
const nextConfig: ConfigResponse = {
...response.config,
subreddits: response.subreddits,
tones: config.tones,
advanced: response.advanced ?? config.advanced,
};
const nextDraft = createSettingsDraft(nextConfig);
setPersistedDraft(nextDraft);
setDraft(nextDraft);
setPendingRiskySave(null);
}
function submit(payload: ConfigUpdateRequest) {
saveMutation.mutate(payload, { onSuccess: applySavedResponse });
}
function updateDraft(nextDraft: SettingsDraft) {
saveMutation.reset();
setErrors({});
setDraft(nextDraft);
}
function prepareSave() {
saveMutation.reset();
const validation = validateSettingsDraft(draft);
setErrors(validation.errors);
if (!validation.data) return;
const reasons: string[] = [];
if (persistedDraft.dryRun && !validation.data.dryRun) {
reasons.push('Automated messages will be sent to Reddit because dry run mode is being disabled.');
}
const persistedLimit = Number(persistedDraft.maxMessagesPerHour);
if (validation.data.maxMessagesPerHour > 20 && validation.data.maxMessagesPerHour > persistedLimit) {
reasons.push(`The limit will allow ${validation.data.maxMessagesPerHour} messages per hour.`);
}
if (reasons.length > 0) {
setPendingRiskySave({ payload: validation.data.payload, reasons });
return;
}
submit(validation.data.payload);
}
return (
<>
{section === 'monitoring' ? <MonitoringSection draft={draft} errors={errors} onChange={updateDraft} /> : null}
{section === 'rate-limits' ? <RateLimitsSection draft={draft} errors={errors} onChange={updateDraft} /> : null}
{section === 'behaviour' ? <BehaviourSection config={config} draft={draft} errors={errors} onChange={updateDraft} /> : null}
{section === 'access' ? <AccessPanel /> : null}
{section !== 'access' ? (
<div className="settings-save-bar">
<div aria-live="polite">
{isDirty ? <span role="status">You have unsaved changes.</span> : <span>All changes saved.</span>}
{saveMutation.isSuccess ? <strong className="save-success">Settings saved.</strong> : null}
{saveMutation.isError ? <strong className="save-error" role="alert">{errorMessage(saveMutation.error)}</strong> : null}
</div>
<div className="header-actions">
<button className="button secondary" disabled={!isDirty || saveMutation.isPending} onClick={() => { setDraft(persistedDraft); setErrors({}); saveMutation.reset(); }} type="button">Reset changes</button>
<button aria-label={saveMutation.isPending ? 'Saving settings' : 'Save settings'} className="button primary" disabled={!isDirty || saveMutation.isPending} onClick={prepareSave} type="button">{saveMutation.isPending ? 'Saving…' : 'Save settings'}</button>
</div>
</div>
) : null}
{pendingRiskySave ? (
<RiskDialog
isPending={saveMutation.isPending}
onCancel={() => setPendingRiskySave(null)}
onConfirm={() => submit(pendingRiskySave.payload)}
reasons={pendingRiskySave.reasons}
/>
) : null}
</>
);
}