diff --git a/frontend/src/melodies/MelodyForm.jsx b/frontend/src/melodies/MelodyForm.jsx
index 304142d..b4cf00f 100644
--- a/frontend/src/melodies/MelodyForm.jsx
+++ b/frontend/src/melodies/MelodyForm.jsx
@@ -225,6 +225,15 @@ export default function MelodyForm() {
if (isEdit) {
const body = buildBody({ dateEdited: now, lastEditedBy: userName, adminNotes });
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 {
const body = buildBody({ dateCreated: now, dateEdited: now, createdBy: userName, lastEditedBy: userName, adminNotes });
const created = await api.post(`/melodies?publish=${publish}`, body);
diff --git a/frontend/src/melodies/archetypes/ArchetypeList.jsx b/frontend/src/melodies/archetypes/ArchetypeList.jsx
index 66932eb..b3bb876 100644
--- a/frontend/src/melodies/archetypes/ArchetypeList.jsx
+++ b/frontend/src/melodies/archetypes/ArchetypeList.jsx
@@ -2,6 +2,7 @@ import { useState, useEffect } from "react";
import { useNavigate, Link } from "react-router-dom";
import api from "../../api/client";
import ConfirmDialog from "../../components/ConfirmDialog";
+import { getLocalizedValue } from "../melodyUtils";
function fallbackCopy(text, onSuccess) {
const ta = document.createElement("textarea");
@@ -67,7 +68,7 @@ function CodeSnippetModal({ melody, onClose }) {
}
// Modal to show melodies that use this archetype
-function AssignedMelodiesModal({ archetype, melodyDetails, onClose }) {
+function AssignedMelodiesModal({ archetype, melodyDetails, primaryLang, onClose }) {
if (!archetype) return null;
return (
-
{m.name || m.id}
+
{getLocalizedValue(m.nameRaw, primaryLang, m.id)}
View →
))
@@ -125,17 +126,45 @@ export default function ArchetypeList() {
const [codeSnippetMelody, setCodeSnippetMelody] = useState(null);
const [assignedModal, setAssignedModal] = useState(null); // { archetype, melodyDetails }
const [loadingAssigned, setLoadingAssigned] = useState(false);
+ const [primaryLang, setPrimaryLang] = useState("en");
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);
setError("");
try {
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) {
setError(err.message);
} finally {
@@ -196,18 +225,8 @@ export default function ArchetypeList() {
.filter((r) => r.status === "fulfilled" && r.value)
.map((r) => {
const m = r.value;
- // Extract a display 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 };
+ return { id: m.id, nameRaw: m.information?.name };
});
- // 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
const missingIds = results
.map((r, i) => r.status === "rejected" || !r.value ? ids[i] : null)
@@ -426,6 +445,7 @@ export default function ArchetypeList() {
setAssignedModal(null)}
/>