Fixed duplicate melody creating
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user