119 lines
4.9 KiB
TypeScript
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}
|
|
</>
|
|
);
|
|
}
|