fix(settings): preserve Rules drafts across internal tabs
This commit is contained in:
@@ -1,13 +1,11 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React from "react";
|
||||
|
||||
import { Alert, Box, Button, TextField, Typography } from "@mui/material";
|
||||
import { PageSection } from "../layout/PageLayout";
|
||||
|
||||
import { api, getApiErrorMessage } from "../api";
|
||||
import { useToast } from "../toast";
|
||||
import { useI18n } from "../i18n/I18nProvider";
|
||||
|
||||
type RuleSettings = {
|
||||
export type RuleSettings = {
|
||||
id: number;
|
||||
appliedFollowUpDays: number;
|
||||
appliedGhostDays: number;
|
||||
@@ -17,61 +15,47 @@ type RuleSettings = {
|
||||
feedbackGhostDays: number;
|
||||
};
|
||||
|
||||
export default function RulesSettingsCard() {
|
||||
const { toast } = useToast();
|
||||
export const RULE_FIELDS = ["appliedFollowUpDays", "appliedGhostDays", "offerFollowUpDays", "offerGhostDays", "feedbackFollowUpDays", "feedbackGhostDays"] as const;
|
||||
export type RuleField = typeof RULE_FIELDS[number];
|
||||
export type RuleDraft = Record<RuleField, string>;
|
||||
|
||||
type Props = {
|
||||
draft: RuleDraft | null;
|
||||
loadError: string | null;
|
||||
saveError: boolean;
|
||||
invalid: RuleField[];
|
||||
saving: boolean;
|
||||
dirty: boolean;
|
||||
onChange: (field: RuleField, value: string) => void;
|
||||
onSave: () => void;
|
||||
onDiscard: () => void;
|
||||
onRetry: () => void;
|
||||
};
|
||||
|
||||
export default function RulesSettingsCard({ draft, loadError, saveError, invalid, saving, dirty, onChange, onSave, onDiscard, onRetry }: Props) {
|
||||
const { t } = useI18n();
|
||||
const [s, setS] = useState<RuleSettings | null>(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [loadError, setLoadError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
api.get<RuleSettings>("/rules")
|
||||
.then((r) => {
|
||||
if (!active) return;
|
||||
setS(r.data);
|
||||
setLoadError(null);
|
||||
})
|
||||
.catch((e: any) => {
|
||||
if (!active) return;
|
||||
setS(null);
|
||||
setLoadError(getApiErrorMessage(e, t("rulesLoadFailed")));
|
||||
});
|
||||
return () => { active = false; };
|
||||
}, [t]);
|
||||
|
||||
const save = async () => {
|
||||
if (!s) return;
|
||||
setSaving(true);
|
||||
try {
|
||||
await api.put("/rules", s);
|
||||
toast(t("rulesSave"), "success");
|
||||
} catch {
|
||||
toast(t("rulesSaveFailed"), "error");
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!s) {
|
||||
if (!draft) {
|
||||
return (
|
||||
<PageSection surface title={t("rulesTitle")}>
|
||||
{loadError ? <Alert severity="error">{loadError}</Alert> : <Typography sx={{ color: "text.secondary" }}>{t("rulesLoading")}</Typography>}
|
||||
{loadError ? <Alert severity="error" action={<Button color="inherit" onClick={onRetry}>{t("retry")}</Button>}>{loadError}</Alert> : <Typography role="status" sx={{ color: "text.secondary" }}>{t("rulesLoading")}</Typography>}
|
||||
</PageSection>
|
||||
);
|
||||
}
|
||||
|
||||
const num = (k: keyof RuleSettings) => ({
|
||||
value: s[k],
|
||||
const num = (k: RuleField) => ({
|
||||
value: draft[k],
|
||||
onChange: (e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setS((p) => (p ? { ...p, [k]: Number(e.target.value) } : p)),
|
||||
onChange(k, e.target.value),
|
||||
type: "number" as const,
|
||||
inputProps: { min: 1, max: 365 },
|
||||
error: invalid.includes(k),
|
||||
helperText: invalid.includes(k) ? t("rulesDaysInvalid") : undefined,
|
||||
inputProps: { min: 1, max: 365, step: 1, required: true },
|
||||
});
|
||||
|
||||
return (
|
||||
<PageSection surface title={t("rulesTitle")} description={t("rulesBody")}>
|
||||
|
||||
<Box component="form" noValidate onSubmit={(event) => { event.preventDefault(); onSave(); }}>
|
||||
{saveError ? <Alert severity="error" sx={{ mb: 2 }}>{t("rulesSaveFailed")}</Alert> : null}
|
||||
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", md: "1fr 1fr 1fr" }, gap: 2 }}>
|
||||
<TextField label={t("rulesAppliedFollowUpDays")} {...num("appliedFollowUpDays")} />
|
||||
<TextField label={t("rulesAppliedGhostDays")} {...num("appliedGhostDays")} />
|
||||
@@ -84,11 +68,13 @@ export default function RulesSettingsCard() {
|
||||
<Box />
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: "flex", justifyContent: "flex-end", mt: 2 }}>
|
||||
<Button variant="contained" onClick={save} disabled={saving}>
|
||||
<Box sx={{ display: "flex", justifyContent: "flex-end", flexWrap: "wrap", gap: 1, mt: 2 }}>
|
||||
<Button onClick={onDiscard} disabled={!dirty || saving}>{t("assetsDiscardChanges")}</Button>
|
||||
<Button variant="contained" type="submit" disabled={saving}>
|
||||
{saving ? t("rulesSaving") : t("rulesSave")}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</PageSection>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user