Fixed duplicate melody creating

This commit is contained in:
2026-02-22 18:52:38 +02:00
parent 02fdcc473f
commit 1ccf855913
2 changed files with 45 additions and 16 deletions

View File

@@ -225,6 +225,15 @@ export default function MelodyForm() {
if (isEdit) { if (isEdit) {
const body = buildBody({ dateEdited: now, lastEditedBy: userName, adminNotes }); const body = buildBody({ dateEdited: now, lastEditedBy: userName, adminNotes });
await api.put(`/melodies/${id}`, body); await api.put(`/melodies/${id}`, body);
} else if (savedMelodyId) {
// Melody was already created (e.g. after using Select Archetype / Build on the Fly)
// Update it instead of creating a duplicate
melodyId = savedMelodyId;
const body = buildBody({ dateEdited: now, lastEditedBy: userName, adminNotes });
await api.put(`/melodies/${savedMelodyId}`, body);
if (publish) {
await api.post(`/melodies/${savedMelodyId}/publish`);
}
} else { } else {
const body = buildBody({ dateCreated: now, dateEdited: now, createdBy: userName, lastEditedBy: userName, adminNotes }); const body = buildBody({ dateCreated: now, dateEdited: now, createdBy: userName, lastEditedBy: userName, adminNotes });
const created = await api.post(`/melodies?publish=${publish}`, body); const created = await api.post(`/melodies?publish=${publish}`, body);

View File

@@ -2,6 +2,7 @@ import { useState, useEffect } from "react";
import { useNavigate, Link } from "react-router-dom"; import { useNavigate, Link } from "react-router-dom";
import api from "../../api/client"; import api from "../../api/client";
import ConfirmDialog from "../../components/ConfirmDialog"; import ConfirmDialog from "../../components/ConfirmDialog";
import { getLocalizedValue } from "../melodyUtils";
function fallbackCopy(text, onSuccess) { function fallbackCopy(text, onSuccess) {
const ta = document.createElement("textarea"); const ta = document.createElement("textarea");
@@ -67,7 +68,7 @@ function CodeSnippetModal({ melody, onClose }) {
} }
// Modal to show melodies that use this archetype // Modal to show melodies that use this archetype
function AssignedMelodiesModal({ archetype, melodyDetails, onClose }) { function AssignedMelodiesModal({ archetype, melodyDetails, primaryLang, onClose }) {
if (!archetype) return null; if (!archetype) return null;
return ( return (
<div <div
@@ -97,7 +98,7 @@ function AssignedMelodiesModal({ archetype, melodyDetails, onClose }) {
className="flex items-center justify-between px-3 py-2 rounded-lg border transition-colors hover:bg-[var(--bg-card-hover)]" className="flex items-center justify-between px-3 py-2 rounded-lg border transition-colors hover:bg-[var(--bg-card-hover)]"
style={{ borderColor: "var(--border-primary)", color: "var(--text-heading)", textDecoration: "none" }} style={{ borderColor: "var(--border-primary)", color: "var(--text-heading)", textDecoration: "none" }}
> >
<span className="text-sm font-medium">{m.name || m.id}</span> <span className="text-sm font-medium">{getLocalizedValue(m.nameRaw, primaryLang, m.id)}</span>
<span className="text-xs" style={{ color: "var(--accent)" }}>View </span> <span className="text-xs" style={{ color: "var(--accent)" }}>View </span>
</Link> </Link>
)) ))
@@ -125,17 +126,45 @@ export default function ArchetypeList() {
const [codeSnippetMelody, setCodeSnippetMelody] = useState(null); const [codeSnippetMelody, setCodeSnippetMelody] = useState(null);
const [assignedModal, setAssignedModal] = useState(null); // { archetype, melodyDetails } const [assignedModal, setAssignedModal] = useState(null); // { archetype, melodyDetails }
const [loadingAssigned, setLoadingAssigned] = useState(false); const [loadingAssigned, setLoadingAssigned] = useState(false);
const [primaryLang, setPrimaryLang] = useState("en");
useEffect(() => { useEffect(() => {
loadArchetypes(); api.get("/settings/melody").then((ms) => setPrimaryLang(ms.primary_language || "en")).catch(() => {});
loadArchetypes({ verify: true });
}, []); }, []);
const loadArchetypes = async () => { const loadArchetypes = async ({ verify = false } = {}) => {
setLoading(true); setLoading(true);
setError(""); setError("");
try { try {
const data = await api.get("/builder/melodies"); const data = await api.get("/builder/melodies");
setArchetypes(data.melodies || []); const list = data.melodies || [];
setArchetypes(list);
if (verify) {
// Check all assigned_melody_ids across all archetypes and prune stale ones
let didPrune = false;
await Promise.all(
list.map(async (archetype) => {
const ids = archetype.assigned_melody_ids || [];
if (ids.length === 0) return;
const results = await Promise.allSettled(ids.map((mid) => api.get(`/melodies/${mid}`)));
const missingIds = results
.map((r, i) => (!r.value || r.status === "rejected" ? ids[i] : null))
.filter(Boolean);
for (const mid of missingIds) {
try {
await api.post(`/builder/melodies/${archetype.id}/unassign?firestore_melody_id=${mid}`);
didPrune = true;
} catch { /* best-effort */ }
}
})
);
if (didPrune) {
const refreshed = await api.get("/builder/melodies");
setArchetypes(refreshed.melodies || []);
}
}
} catch (err) { } catch (err) {
setError(err.message); setError(err.message);
} finally { } finally {
@@ -196,18 +225,8 @@ export default function ArchetypeList() {
.filter((r) => r.status === "fulfilled" && r.value) .filter((r) => r.status === "fulfilled" && r.value)
.map((r) => { .map((r) => {
const m = r.value; const m = r.value;
// Extract a display name return { id: m.id, nameRaw: m.information?.name };
const nameRaw = m.information?.name;
let name = "";
if (typeof nameRaw === "string") name = nameRaw;
else if (nameRaw && typeof nameRaw === "object") name = Object.values(nameRaw)[0] || m.id;
return { id: m.id, name: name || m.pid || m.id };
}); });
// IDs that were NOT found (melody deleted)
const foundIds = results
.filter((r, i) => r.status === "fulfilled" && r.value)
.map((_, i) => ids[results.findIndex((r, j) => j === i && r.status === "fulfilled")]);
// Unassign any IDs that no longer exist // Unassign any IDs that no longer exist
const missingIds = results const missingIds = results
.map((r, i) => r.status === "rejected" || !r.value ? ids[i] : null) .map((r, i) => r.status === "rejected" || !r.value ? ids[i] : null)
@@ -426,6 +445,7 @@ export default function ArchetypeList() {
<AssignedMelodiesModal <AssignedMelodiesModal
archetype={assignedModal?.archetype} archetype={assignedModal?.archetype}
melodyDetails={assignedModal?.melodyDetails || []} melodyDetails={assignedModal?.melodyDetails || []}
primaryLang={primaryLang}
onClose={() => setAssignedModal(null)} onClose={() => setAssignedModal(null)}
/> />
</div> </div>