refactor(ui): shared DraftSaveBar (Settings + Config save bars)

One Save/Discard bar with an inline (Settings card) and floating (Config page)
variant, replacing the two near-identical hand-rolled bars + their state machine.
This commit is contained in:
npeter83 2026-06-29 00:07:37 +02:00
parent 53c75f69e2
commit f6b9ac2dd1
3 changed files with 109 additions and 66 deletions

View file

@ -1,12 +1,13 @@
import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { Check, RotateCcw, Save, Send } from "lucide-react";
import { RotateCcw, Send } from "lucide-react";
import { api, type ConfigItem } from "../lib/api";
import { notify } from "../lib/notifications";
import Tooltip from "./Tooltip";
import Tabs, { usePersistedTab } from "./Tabs";
import { Switch } from "./ui/form";
import { DraftSaveBar } from "./ui/DraftSaveBar";
// Admin Configuration page: edit DB-overridable operational settings (registry-driven from the
// backend — adding a key there makes it appear here automatically). Edits are drafted and
@ -151,40 +152,21 @@ export default function ConfigPanel() {
</div>
)}
{(dirty || saveState !== "idle") && (
<div className="fixed bottom-4 left-1/2 -translate-x-1/2 z-40 glass rounded-2xl shadow-lg flex items-center justify-between gap-4 px-4 py-3 w-[min(48rem,calc(100%-2rem))]">
<span className="text-sm text-muted">
{saveState === "saved" ? (
<span className="text-emerald-500 inline-flex items-center gap-1.5">
<Check className="w-4 h-4" />
{t("config.saved")}
</span>
) : saveState === "error" ? (
<span className="text-red-500">{t("config.saveFailed")}</span>
) : (
t("config.unsaved")
)}
</span>
<div className="flex items-center gap-2">
<button
onClick={discard}
disabled={saveState === "saving" || !dirty}
className="glass-card glass-hover flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm disabled:opacity-40 transition"
>
<RotateCcw className="w-4 h-4" />
{t("config.discard")}
</button>
<button
onClick={save}
disabled={saveState === "saving" || !dirty}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm bg-accent text-accent-fg font-medium shadow-md shadow-accent/20 disabled:opacity-40 transition"
>
<Save className="w-4 h-4" />
{saveState === "saving" ? t("config.saving") : t("config.save")}
</button>
</div>
</div>
)}
<DraftSaveBar
variant="floating"
dirty={dirty}
state={saveState}
onSave={save}
onDiscard={discard}
labels={{
saved: t("config.saved"),
failed: t("config.saveFailed"),
unsaved: t("config.unsaved"),
discard: t("config.discard"),
save: t("config.save"),
saving: t("config.saving"),
}}
/>
</div>
);
}