Phase 10H: Scene Links Editing
This commit is contained in:
parent
8d6399358e
commit
500c70cb58
@ -110,17 +110,22 @@
|
||||
</section>
|
||||
|
||||
<section class="word-companion-section word-companion-link-groups" data-plotdirector-link-groups hidden aria-label="Scene Links">
|
||||
<div class="word-companion-section-header">
|
||||
<span>Scene Links</span>
|
||||
<button type="button" data-save-scene-links disabled>Save Scene Links</button>
|
||||
</div>
|
||||
<p class="word-companion-message" data-scene-links-status>No resolved PlotDirector scene.</p>
|
||||
<div>
|
||||
<span>Characters</span>
|
||||
<div class="word-companion-chip-list" data-character-chips></div>
|
||||
<div class="word-companion-check-list" data-character-chips></div>
|
||||
</div>
|
||||
<div>
|
||||
<span>Assets</span>
|
||||
<div class="word-companion-chip-list" data-asset-chips></div>
|
||||
<div class="word-companion-check-list" data-asset-chips></div>
|
||||
</div>
|
||||
<div>
|
||||
<span>Locations</span>
|
||||
<div class="word-companion-chip-list" data-location-chips></div>
|
||||
<div class="word-companion-check-list" data-location-chips></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@ -284,27 +284,48 @@ body {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.word-companion-chip-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
.word-companion-check-list {
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.word-companion-chip,
|
||||
.word-companion-empty-chip {
|
||||
border: 1px solid rgba(47, 111, 99, 0.25);
|
||||
border-radius: 999px;
|
||||
padding: 3px 7px;
|
||||
background: var(--companion-soft);
|
||||
color: var(--companion-accent-dark);
|
||||
font-size: 0.82rem;
|
||||
font-weight: 700;
|
||||
.word-companion-check-item {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
min-height: 30px;
|
||||
border: 1px solid rgba(47, 111, 99, 0.18);
|
||||
border-radius: 6px;
|
||||
padding: 5px 7px;
|
||||
background: #ffffff;
|
||||
color: var(--companion-ink);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.word-companion-empty-chip {
|
||||
border-color: var(--companion-line);
|
||||
color: var(--companion-muted);
|
||||
.word-companion-check-item input {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: 0;
|
||||
accent-color: var(--companion-accent);
|
||||
}
|
||||
|
||||
.word-companion-check-item span {
|
||||
color: var(--companion-ink);
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.word-companion-check-item:has(input:disabled) {
|
||||
background: var(--companion-soft);
|
||||
color: var(--companion-muted);
|
||||
}
|
||||
|
||||
.word-companion-empty-list {
|
||||
margin: 0;
|
||||
color: var(--companion-muted);
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
.word-companion-outline {
|
||||
|
||||
@ -43,6 +43,8 @@
|
||||
const characterChips = document.querySelector("[data-character-chips]");
|
||||
const assetChips = document.querySelector("[data-asset-chips]");
|
||||
const locationChips = document.querySelector("[data-location-chips]");
|
||||
const saveSceneLinksButton = document.querySelector("[data-save-scene-links]");
|
||||
const sceneLinksStatus = document.querySelector("[data-scene-links-status]");
|
||||
const resolveProjectId = document.querySelector("[data-resolve-project-id]");
|
||||
const resolveProjectTitle = document.querySelector("[data-resolve-project-title]");
|
||||
const resolveBookId = document.querySelector("[data-resolve-book-id]");
|
||||
@ -75,6 +77,11 @@
|
||||
let detectedSceneTitle = "";
|
||||
let detectedSceneWordCount = null;
|
||||
let resolvedSceneId = null;
|
||||
let currentSceneLinks = {
|
||||
characters: [],
|
||||
assets: [],
|
||||
locations: []
|
||||
};
|
||||
|
||||
const setOfficeStatus = (message) => {
|
||||
if (officeStatus) {
|
||||
@ -147,7 +154,11 @@
|
||||
if (syncSceneProgressButton) {
|
||||
syncSceneProgressButton.disabled = !resolvedSceneId;
|
||||
}
|
||||
if (saveSceneLinksButton) {
|
||||
saveSceneLinksButton.disabled = !resolvedSceneId;
|
||||
}
|
||||
setSyncStatus(resolvedSceneId ? "Ready to sync." : "No resolved PlotDirector scene.");
|
||||
setSceneLinksStatus(resolvedSceneId ? "Ready to save." : "No resolved PlotDirector scene.");
|
||||
};
|
||||
|
||||
const setRefreshCurrentSceneBusy = (isBusy) => {
|
||||
@ -167,11 +178,19 @@
|
||||
return String(value);
|
||||
};
|
||||
|
||||
const setSceneLinksStatus = (message) => {
|
||||
setText(sceneLinksStatus, message);
|
||||
};
|
||||
|
||||
const resetPlotDirectorScene = (message) => {
|
||||
setPlotDirectorSceneStatus(message);
|
||||
setHidden(plotDirectorSceneDetails, true);
|
||||
setHidden(writingBriefBlock, true);
|
||||
setHidden(plotDirectorLinkGroups, true);
|
||||
currentSceneLinks = { characters: [], assets: [], locations: [] };
|
||||
renderSceneLinkList(characterChips, [], "character");
|
||||
renderSceneLinkList(assetChips, [], "asset");
|
||||
renderSceneLinkList(locationChips, [], "location");
|
||||
setResolvedScene(null);
|
||||
setText(syncPlotDirectorCount, "-");
|
||||
setText(plotDirectorSceneTitle, "-");
|
||||
@ -183,26 +202,53 @@
|
||||
setHidden(plotDirectorBlockedReasonRow, true);
|
||||
};
|
||||
|
||||
const renderLinkedItems = (container, items) => {
|
||||
const linkItemId = (item, type) => {
|
||||
if (type === "asset") {
|
||||
return item?.assetId || item?.AssetId || item?.storyAssetId || item?.StoryAssetId || 0;
|
||||
}
|
||||
|
||||
if (type === "location") {
|
||||
return item?.locationId || item?.LocationId || 0;
|
||||
}
|
||||
|
||||
return item?.characterId || item?.CharacterId || 0;
|
||||
};
|
||||
|
||||
const renderSceneLinkList = (container, items, type, enabled = false) => {
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = "";
|
||||
const linkedItems = Array.isArray(items) ? items.filter((item) => item.linked) : [];
|
||||
if (linkedItems.length === 0) {
|
||||
const empty = document.createElement("span");
|
||||
empty.className = "word-companion-empty-chip";
|
||||
const rows = Array.isArray(items) ? items : [];
|
||||
if (rows.length === 0) {
|
||||
const empty = document.createElement("p");
|
||||
empty.className = "word-companion-empty-list";
|
||||
empty.textContent = "None";
|
||||
container.append(empty);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const item of linkedItems) {
|
||||
const chip = document.createElement("span");
|
||||
chip.className = "word-companion-chip";
|
||||
chip.textContent = item.name || "Unnamed";
|
||||
container.append(chip);
|
||||
for (const item of rows) {
|
||||
const id = Number.parseInt(linkItemId(item, type), 10);
|
||||
if (!Number.isInteger(id) || id <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const label = document.createElement("label");
|
||||
label.className = "word-companion-check-item";
|
||||
|
||||
const checkbox = document.createElement("input");
|
||||
checkbox.type = "checkbox";
|
||||
checkbox.value = String(id);
|
||||
checkbox.checked = !!item.linked;
|
||||
checkbox.disabled = !enabled;
|
||||
|
||||
const text = document.createElement("span");
|
||||
text.textContent = item.name || "Unnamed";
|
||||
|
||||
label.append(checkbox, text);
|
||||
container.append(label);
|
||||
}
|
||||
};
|
||||
|
||||
@ -226,9 +272,15 @@
|
||||
writingBrief.textContent = data?.writingBrief || "No writing brief has been added for this scene.";
|
||||
}
|
||||
|
||||
renderLinkedItems(characterChips, data?.characters);
|
||||
renderLinkedItems(assetChips, data?.assets);
|
||||
renderLinkedItems(locationChips, data?.locations);
|
||||
currentSceneLinks = {
|
||||
characters: Array.isArray(data?.characters) ? data.characters : [],
|
||||
assets: Array.isArray(data?.assets) ? data.assets : [],
|
||||
locations: Array.isArray(data?.locations) ? data.locations : []
|
||||
};
|
||||
renderSceneLinkList(characterChips, currentSceneLinks.characters, "character", !!resolvedSceneId);
|
||||
renderSceneLinkList(assetChips, currentSceneLinks.assets, "asset", !!resolvedSceneId);
|
||||
renderSceneLinkList(locationChips, currentSceneLinks.locations, "location", !!resolvedSceneId);
|
||||
setSceneLinksStatus(resolvedSceneId ? "Ready to save." : "No resolved PlotDirector scene.");
|
||||
setHidden(plotDirectorSceneDetails, false);
|
||||
setHidden(writingBriefBlock, false);
|
||||
setHidden(plotDirectorLinkGroups, false);
|
||||
@ -711,6 +763,63 @@
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
|
||||
const putJson = (url, body) => fetchJson(url, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
|
||||
const selectedLinkIds = (container) => {
|
||||
if (!container) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [...container.querySelectorAll("input[type='checkbox']:checked")]
|
||||
.map((checkbox) => Number.parseInt(checkbox.value || "", 10))
|
||||
.filter((id) => Number.isInteger(id) && id > 0);
|
||||
};
|
||||
|
||||
const updateCurrentSceneLinkState = () => {
|
||||
const selectedCharacters = new Set(selectedLinkIds(characterChips));
|
||||
const selectedAssets = new Set(selectedLinkIds(assetChips));
|
||||
const selectedLocations = new Set(selectedLinkIds(locationChips));
|
||||
|
||||
currentSceneLinks = {
|
||||
characters: currentSceneLinks.characters.map((item) => ({ ...item, linked: selectedCharacters.has(Number.parseInt(linkItemId(item, "character"), 10)) })),
|
||||
assets: currentSceneLinks.assets.map((item) => ({ ...item, linked: selectedAssets.has(Number.parseInt(linkItemId(item, "asset"), 10)) })),
|
||||
locations: currentSceneLinks.locations.map((item) => ({ ...item, linked: selectedLocations.has(Number.parseInt(linkItemId(item, "location"), 10)) }))
|
||||
};
|
||||
};
|
||||
|
||||
const saveSceneLinks = async () => {
|
||||
if (!resolvedSceneId) {
|
||||
setSceneLinksStatus("No resolved PlotDirector scene.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (saveSceneLinksButton) {
|
||||
saveSceneLinksButton.disabled = true;
|
||||
saveSceneLinksButton.textContent = "Saving...";
|
||||
}
|
||||
|
||||
try {
|
||||
await putJson(`/api/word-companion/scenes/${resolvedSceneId}/links`, {
|
||||
characterIds: selectedLinkIds(characterChips),
|
||||
assetIds: selectedLinkIds(assetChips),
|
||||
locationIds: selectedLinkIds(locationChips)
|
||||
});
|
||||
updateCurrentSceneLinkState();
|
||||
setSceneLinksStatus("Scene links saved successfully.");
|
||||
} catch {
|
||||
setSceneLinksStatus("Unable to save scene links.");
|
||||
} finally {
|
||||
if (saveSceneLinksButton) {
|
||||
saveSceneLinksButton.disabled = !resolvedSceneId;
|
||||
saveSceneLinksButton.textContent = "Save Scene Links";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const syncSceneProgress = async () => {
|
||||
if (!resolvedSceneId) {
|
||||
setSyncStatus("No resolved PlotDirector scene.");
|
||||
@ -836,8 +945,8 @@
|
||||
}
|
||||
setPlotDirectorSceneStatus("Linked to PlotDirector");
|
||||
setDocumentMessage("");
|
||||
renderCompanionData(companionData);
|
||||
setResolvedScene(resolvedSceneId);
|
||||
renderCompanionData(companionData);
|
||||
return true;
|
||||
} catch {
|
||||
resetPlotDirectorScene("Not found in PlotDirector");
|
||||
@ -985,6 +1094,7 @@
|
||||
refreshPlotDirectorSceneButton?.addEventListener("click", refreshPlotDirectorScene);
|
||||
refreshCurrentSceneButton?.addEventListener("click", refreshCurrentScene);
|
||||
syncSceneProgressButton?.addEventListener("click", syncSceneProgress);
|
||||
saveSceneLinksButton?.addEventListener("click", saveSceneLinks);
|
||||
runDiagnosticsButton?.addEventListener("click", runDiagnostics);
|
||||
|
||||
if (isAuthenticated) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user