Added Draft Melodies. Further improvements to the UI

This commit is contained in:
2026-02-18 19:33:59 +02:00
parent a6e0b1d46e
commit aad8942d65
12 changed files with 1080 additions and 518 deletions

View File

@@ -16,6 +16,7 @@ const MELODY_TONES = ["", "normal", "festive", "cheerful", "lamentation"];
// All available columns with their defaults
const ALL_COLUMNS = [
{ key: "status", label: "Status", defaultOn: true },
{ key: "color", label: "Color", defaultOn: true },
{ key: "name", label: "Name", defaultOn: true, alwaysOn: true },
{ key: "description", label: "Description", defaultOn: false },
@@ -56,9 +57,12 @@ export default function MelodyList() {
const [search, setSearch] = useState("");
const [typeFilter, setTypeFilter] = useState("");
const [toneFilter, setToneFilter] = useState("");
const [statusFilter, setStatusFilter] = useState("");
const [displayLang, setDisplayLang] = useState("en");
const [melodySettings, setMelodySettings] = useState(null);
const [deleteTarget, setDeleteTarget] = useState(null);
const [unpublishTarget, setUnpublishTarget] = useState(null);
const [actionLoading, setActionLoading] = useState(null);
const [visibleColumns, setVisibleColumns] = useState(getDefaultVisibleColumns);
const [showColumnPicker, setShowColumnPicker] = useState(false);
const columnPickerRef = useRef(null);
@@ -94,6 +98,7 @@ export default function MelodyList() {
if (search) params.set("search", search);
if (typeFilter) params.set("type", typeFilter);
if (toneFilter) params.set("tone", toneFilter);
if (statusFilter) params.set("status", statusFilter);
const qs = params.toString();
const data = await api.get(`/melodies${qs ? `?${qs}` : ""}`);
setMelodies(data.melodies);
@@ -107,7 +112,7 @@ export default function MelodyList() {
useEffect(() => {
fetchMelodies();
}, [search, typeFilter, toneFilter]);
}, [search, typeFilter, toneFilter, statusFilter]);
const handleDelete = async () => {
if (!deleteTarget) return;
@@ -121,6 +126,33 @@ export default function MelodyList() {
}
};
const handlePublish = async (row) => {
setActionLoading(row.id);
try {
await api.post(`/melodies/${row.id}/publish`);
fetchMelodies();
} catch (err) {
setError(err.message);
} finally {
setActionLoading(null);
}
};
const handleUnpublish = async () => {
if (!unpublishTarget) return;
setActionLoading(unpublishTarget.id);
try {
await api.post(`/melodies/${unpublishTarget.id}/unpublish`);
setUnpublishTarget(null);
fetchMelodies();
} catch (err) {
setError(err.message);
setUnpublishTarget(null);
} finally {
setActionLoading(null);
}
};
const toggleColumn = (key) => {
const col = ALL_COLUMNS.find((c) => c.key === key);
if (col?.alwaysOn) return;
@@ -142,6 +174,18 @@ export default function MelodyList() {
const info = row.information || {};
const ds = row.default_settings || {};
switch (key) {
case "status":
return (
<span
className="px-2 py-0.5 text-xs font-semibold rounded-full"
style={row.status === "published"
? { backgroundColor: "rgba(22,163,74,0.15)", color: "#22c55e" }
: { backgroundColor: "rgba(156,163,175,0.15)", color: "#9ca3af" }
}
>
{row.status === "published" ? "Live" : "Draft"}
</span>
);
case "color":
return info.color ? (
<span
@@ -293,6 +337,15 @@ export default function MelodyList() {
</option>
))}
</select>
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className={selectClass}
>
<option value="">All Statuses</option>
<option value="published">Published (Live)</option>
<option value="draft">Drafts</option>
</select>
{languages.length > 1 && (
<select
value={displayLang}
@@ -440,6 +493,25 @@ export default function MelodyList() {
className="flex gap-2"
onClick={(e) => e.stopPropagation()}
>
{row.status === "draft" ? (
<button
onClick={() => handlePublish(row)}
disabled={actionLoading === row.id}
className="text-xs cursor-pointer disabled:opacity-50"
style={{ color: "#22c55e" }}
>
{actionLoading === row.id ? "..." : "Publish"}
</button>
) : (
<button
onClick={() => setUnpublishTarget(row)}
disabled={actionLoading === row.id}
className="text-xs cursor-pointer disabled:opacity-50"
style={{ color: "#ea580c" }}
>
Unpublish
</button>
)}
<button
onClick={() => navigate(`/melodies/${row.id}/edit`)}
className="text-xs cursor-pointer"
@@ -472,6 +544,14 @@ export default function MelodyList() {
onConfirm={handleDelete}
onCancel={() => setDeleteTarget(null)}
/>
<ConfirmDialog
open={!!unpublishTarget}
title="Unpublish Melody"
message={`This melody may be in use by devices. Unpublishing will remove "${getDisplayName(unpublishTarget?.information?.name)}" from Firestore and devices will no longer have access. The melody will be kept as a draft. Continue?`}
onConfirm={handleUnpublish}
onCancel={() => setUnpublishTarget(null)}
/>
</div>
);
}