From 6bd5365cc22f84c047c1585e3f54571af46f53e5 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Sun, 5 Jul 2026 18:52:51 -0400 Subject: feat: renameable directories and files in admin gui --- internal/admin/static/admin.css | 11 ++++ internal/admin/static/admin.js | 65 ++++++++++++++++++++ internal/admin/static/cardeditor.js | 6 +- internal/admin/static/dropeditor.js | 5 +- internal/admin/static/editor.js | 108 +++++++++++++++++++++++++++++++--- internal/admin/static/hazardeditor.js | 5 +- internal/admin/static/itemeditor.js | 17 ++++-- internal/admin/static/mobeditor.js | 8 ++- internal/admin/static/moduleeditor.js | 8 ++- internal/admin/static/objecteditor.js | 23 +++++--- internal/admin/static/techeditor.js | 2 +- 11 files changed, 221 insertions(+), 37 deletions(-) (limited to 'internal/admin/static') diff --git a/internal/admin/static/admin.css b/internal/admin/static/admin.css index 3c6aaa4..bef3589 100644 --- a/internal/admin/static/admin.css +++ b/internal/admin/static/admin.css @@ -231,6 +231,17 @@ g.ud-hover:hover text{font-weight:bold} .cm-menu button:hover{background:var(--accent)} .cm-menu button.danger{color:#f55} .cm-menu button.danger:hover{background:var(--danger)} +.rename-header{display:flex;align-items:center;gap:6px} +.rename-header-text{margin-bottom:0!important;padding-bottom:0!important;border-bottom:none!important} +.rename-pencil{font-size:14px;cursor:pointer;color:#888;padding:2px 4px;border-radius:3px;transition:color .15s,transform .15s} +.rename-pencil:hover{color:#fff;transform:scale(1.1)} +.rename-edit-row{display:flex;align-items:center;gap:4px} +.rename-input{flex:1;padding:4px 8px;font-size:14px;font-weight:bold;background:var(--input-bg);color:var(--text);border:1px solid var(--accent);border-radius:3px;font-family:monospace} +.rename-btn-confirm,.rename-btn-cancel{padding:2px 8px;font-size:13px;border-radius:3px;cursor:pointer;font-family:monospace;line-height:1} +.rename-btn-confirm{background:var(--success);color:#fff;border:none} +.rename-btn-cancel{background:var(--danger);color:#fff;border:none} +.item-rename-input{padding:4px 6px;font-size:12px;background:var(--input-bg);color:var(--text);border:1px solid var(--accent);border-radius:3px;font-family:monospace;width:100%} + .panel-left{width:200px;background:var(--panel);border-right:1px solid var(--border);overflow-y:auto;display:flex;flex-direction:column;flex-shrink:0} .panel-left .panel-header{font-size:11px;color:#aaa;text-transform:uppercase;letter-spacing:.5px;padding:12px 12px 8px;border-bottom:1px solid var(--border);flex-shrink:0} .panel-left .dc-item{display:block;padding:6px 12px;font-size:11px;cursor:pointer;color:var(--text);border-bottom:1px solid rgba(255,255,255,.03);font-family:monospace} diff --git a/internal/admin/static/admin.js b/internal/admin/static/admin.js index f8e9d13..a6bb8b2 100644 --- a/internal/admin/static/admin.js +++ b/internal/admin/static/admin.js @@ -52,4 +52,69 @@ window.saveForm = function(url, data, msg) { }).catch(function(e) { notify('Save failed: '+e.message, 'error'); }); }; +window.entityRename = function(type, oldID, newID) { + return API.post('/api/' + type + '/rename', {id: oldID, new_id: newID}).then(function(r) { + notify('Renamed ' + oldID + ' to ' + newID, 'success'); + updateUndoBar(); + return r; + }); +}; + +window.renderRenameableHeader = function(label, id) { + var h = '
'; + h += '

' + esc(label) + ': ' + esc(id) + '

'; + h += '\u270E'; + h += ''; + h += '
'; + return h; +}; + +window.handleHeaderRename = function(oldID, newID) { + var type = window.editorType || ''; + if (!type) return; + entityRename(type, oldID, newID).then(function() { + window.currentID = newID; + var typeSingular = type.slice(0, -1) + 'ID'; + if (window[typeSingular] !== undefined) window[typeSingular] = newID; + if (window.loadList) window.loadList(); + if (window.loadItem) window.loadItem(newID); + }).catch(function(e) { notify('Rename failed: ' + e.message, 'error'); }); +}; + +window.startHeaderRename = function(pencilEl, oldID) { + var header = pencilEl.parentElement; + var textSpan = header.querySelector('.rename-header-text'); + var editRow = header.querySelector('.rename-edit-row'); + + textSpan.style.display = 'none'; + pencilEl.style.display = 'none'; + editRow.style.display = 'flex'; + editRow.innerHTML = '' + + '' + + ''; + + var input = editRow.querySelector('.rename-input'); + var confirmBtn = editRow.querySelector('.rename-btn-confirm'); + var cancelBtn = editRow.querySelector('.rename-btn-cancel'); + + input.focus(); + input.select(); + + function cancel() { + textSpan.style.display = ''; + pencilEl.style.display = ''; + editRow.style.display = 'none'; + } + + function confirm() { + var newID = input.value.trim(); + if (!newID || newID === oldID) { cancel(); return; } + if (window.handleHeaderRename) handleHeaderRename(oldID, newID); + } + + cancelBtn.onclick = cancel; + input.onkeydown = function(e) { if (e.key === 'Enter') confirm(); else if (e.key === 'Escape') cancel(); }; + confirmBtn.onclick = confirm; +}; + document.addEventListener('DOMContentLoaded', function() { updateUndoBar(); }); diff --git a/internal/admin/static/cardeditor.js b/internal/admin/static/cardeditor.js index 8ff5eaf..5e0c7cb 100644 --- a/internal/admin/static/cardeditor.js +++ b/internal/admin/static/cardeditor.js @@ -199,13 +199,13 @@ function ced_setPathInData(ed, dataPath, value) { if (!ed || !dataPath) return; var m = dataPath.match(/^(.+)\[(\d+)\]\.(.+)$/); if (m) { - if (!ed[m[1]]) ed[m[1]] = []; - if (!ed[m[1]][m[2]]) ed[m[1]][m[2]] = {}; + if (!ed[m[1]] || !Array.isArray(ed[m[1]])) return; + if (!ed[m[1]][m[2]]) return; ed[m[1]][m[2]][m[3]] = value; } else { var m2 = dataPath.match(/^(.+)\.(.+)$/); if (m2) { - if (!ed[m2[1]]) ed[m2[1]] = {}; + if (ed[m2[1]] === undefined || ed[m2[1]] === null) return; ed[m2[1]][m2[2]] = value; } else { ed[dataPath] = value; diff --git a/internal/admin/static/dropeditor.js b/internal/admin/static/dropeditor.js index 8e463ec..c5da314 100644 --- a/internal/admin/static/dropeditor.js +++ b/internal/admin/static/dropeditor.js @@ -56,7 +56,7 @@ function loadDropEditor(id) { function loadItem(id) { loadDropEditor(id); } function renderDropEditor(id, data) { - var html = '

Drop Table: ' + esc(id) + '

'; + var html = renderRenameableHeader('Drop Table', id); html += '
'; html += '
'; @@ -155,9 +155,10 @@ function addArrayItem(cardID) { function removeArrayItem(cardID, idx) { var sec = DROP_SECTIONS.find(function(s) { return s.id === cardID; }); if (!sec || !sec.isArray) return; + ced_syncDOMToData(dropData); var arr = dropData[sec.path] || []; arr.splice(idx, 1); - renderCurrent(); + renderDropEditor(dropID, dropData); } function saveDrop() { diff --git a/internal/admin/static/editor.js b/internal/admin/static/editor.js index 5b2d8ef..a9c1e1c 100644 --- a/internal/admin/static/editor.js +++ b/internal/admin/static/editor.js @@ -71,7 +71,7 @@ function renderDirGroup(name, ids, filter) { } h += ''; h += ''; @@ -272,33 +272,123 @@ function createNewDir() { function showDirContextMenu(e, dir) { e.preventDefault(); + closeContextMenu(); if (dir === 'Root') return; var overlay = document.createElement('div'); overlay.className = 'cm-overlay'; - overlay.onclick = function() { overlay.remove(); menu.remove(); }; + overlay.id = '_ctxMenuOverlay'; + overlay.onclick = function() { closeContextMenu(); }; var menu = document.createElement('div'); menu.className = 'cm-menu'; + menu.id = '_ctxMenu'; menu.style.left = e.clientX + 'px'; menu.style.top = e.clientY + 'px'; - var delBtn = document.createElement('button'); - delBtn.textContent = 'Delete Directory'; - delBtn.className = 'danger'; - delBtn.onclick = function() { - overlay.remove(); menu.remove(); + function addBtn(label, cls, handler) { + var btn = document.createElement('button'); + btn.textContent = label; + if (cls) btn.className = cls; + btn.onclick = function() { closeContextMenu(); handler(); }; + menu.appendChild(btn); + } + + addBtn('Rename Directory', '', function() { renameDirInline(dir); }); + addBtn('Delete Directory', 'danger', function() { API.post('/api/' + editorType + '/dirs', {action: 'delete', dir: dir}).then(function(r) { notify('Deleted directory ' + dir + (r.moved ? ' (' + r.moved + ' files moved to root)' : ''), 'success'); updateUndoBar(); loadList(); }).catch(function(e) { notify('Delete failed: ' + e.message, 'error'); }); - }; - menu.appendChild(delBtn); + }); document.body.appendChild(overlay); document.body.appendChild(menu); } +function renameDirInline(dir) { + var name = prompt('New name for directory "' + dir + '":', dir); + if (!name || name === dir) return; + API.post('/api/' + editorType + '/dirs', {action: 'rename_dir', dir: dir, new_name: name}).then(function() { + notify('Renamed directory ' + dir + ' to ' + name, 'success'); + loadList(); + }).catch(function(e) { notify('Rename failed: ' + e.message, 'error'); }); +} + +function showItemContextMenu(e, id) { + e.preventDefault(); + e.stopPropagation(); + closeContextMenu(); + + var overlay = document.createElement('div'); + overlay.className = 'cm-overlay'; + overlay.id = '_ctxMenuOverlay'; + overlay.onclick = function() { closeContextMenu(); }; + + var menu = document.createElement('div'); + menu.className = 'cm-menu'; + menu.id = '_ctxMenu'; + menu.style.left = e.clientX + 'px'; + menu.style.top = e.clientY + 'px'; + + function addBtn(label, cls, handler) { + var btn = document.createElement('button'); + btn.textContent = label; + if (cls) btn.className = cls; + btn.onclick = function() { closeContextMenu(); handler(); }; + menu.appendChild(btn); + } + + addBtn('Rename', '', function() { startInlineItemRename(id); }); + addBtn('Delete', 'danger', function() { deleteItem(id); }); + + document.body.appendChild(overlay); + document.body.appendChild(menu); +} + +function closeContextMenu() { + var o = document.getElementById('_ctxMenuOverlay'); + var m = document.getElementById('_ctxMenu'); + if (o) o.remove(); + if (m) m.remove(); +} + +function startInlineItemRename(id) { + var el = document.querySelector('.item.active'); + if (!el) return; + var origHTML = el.innerHTML; + el.innerHTML = ''; + var input = el.querySelector('.item-rename-input'); + input.focus(); + input.select(); + + function finish(newID) { + newID = (newID || '').trim(); + if (!newID || newID === id) { el.innerHTML = origHTML; return; } + if (allIDs.indexOf(newID) >= 0) { + notify('An entity named "' + newID + '" already exists.', 'error'); + el.innerHTML = origHTML; + return; + } + entityRename(editorType, id, newID).then(function() { + if (currentID === id) currentID = newID; + var typeIDVar = editorType.slice(0, -1) + 'ID'; + if (window[typeIDVar] === id) window[typeIDVar] = newID; + loadList(); + if (currentID === newID) loadItem(newID); + }).catch(function(e) { + notify('Rename failed: ' + e.message, 'error'); + el.innerHTML = origHTML; + }); + } + + input.onkeydown = function(e) { + if (e.key === 'Enter') { input.blur(); } + else if (e.key === 'Escape') { el.innerHTML = origHTML; } + }; + input.onblur = function() { finish(input.value); }; +} + function dirDragOver(e, dir) { e.preventDefault(); e.stopPropagation(); diff --git a/internal/admin/static/hazardeditor.js b/internal/admin/static/hazardeditor.js index a0610dc..ff2857d 100644 --- a/internal/admin/static/hazardeditor.js +++ b/internal/admin/static/hazardeditor.js @@ -66,7 +66,7 @@ function loadHazardEditor(id) { function loadItem(id) { loadHazardEditor(id); } function renderHazardEditor(id, data) { - var html = '

Hazard: ' + esc(id) + '

'; + var html = renderRenameableHeader('Hazard', id); html += '
'; html += '
'; @@ -156,9 +156,10 @@ function toggleCardWidth(id) { function removeSection(id) { var sec = HAZARD_SECTIONS.find(function(s) { return s.id === id; }); if (!sec) return; + ced_syncDOMToData(hazardData); if (sec.fields) sec.fields.forEach(function(f) { delete hazardData[f[0]]; }); delete addedSections[id]; - renderCurrent(); + renderHazardEditor(hazardID, hazardData); } function addSection() { diff --git a/internal/admin/static/itemeditor.js b/internal/admin/static/itemeditor.js index 5ffb0cb..ac76f94 100644 --- a/internal/admin/static/itemeditor.js +++ b/internal/admin/static/itemeditor.js @@ -174,7 +174,7 @@ function loadItemEditor(id) { } function renderItemEditor(id, data) { - var html = '

Item: ' + esc(id) + '

'; + var html = renderRenameableHeader('Item', id); html += '
'; html += '
'; @@ -460,8 +460,9 @@ function addEquipBlock(blockKey) { } function removeEquipBlock(blockKey) { + ced_syncDOMToData(itemData); if (itemData.equipment) delete itemData.equipment[blockKey]; - renderCurrent(); + renderItemEditor(itemID, itemData); } // ========================================================================= @@ -902,6 +903,7 @@ function toggleCardWidth(id) { function removeSection(id) { var sec = SECTIONS.find(function(s) { return s.id === id; }); if (!sec) return; + ced_syncDOMToData(itemData); if (sec.path) { delete itemData[sec.path]; } else { @@ -910,7 +912,7 @@ function removeSection(id) { if (sec.subtables) sec.subtables.forEach(function(st) { delete itemData[st.key]; }); } delete addedSections[id]; - renderCurrent(); + renderItemEditor(itemID, itemData); } function addSection() { @@ -952,9 +954,10 @@ function addArrayItem(cardID) { function removeArrayItem(cardID, idx) { var sec = SECTIONS.find(function(s) { return s.id === cardID; }); if (!sec || !sec.isArray) return; + ced_syncDOMToData(itemData); var arr = itemData[sec.path] || []; arr.splice(idx, 1); - renderCurrent(); + renderItemEditor(itemID, itemData); } // ========================================================================= @@ -991,6 +994,7 @@ function addSubRow(cardID, subKey) { function removeSubRow(cardID, subKey, idx) { var sec = SECTIONS.find(function(s) { return s.id === cardID; }); if (!sec) return; + ced_syncDOMToData(itemData); var p = cardPath(sec); var sectionRoot = p ? (itemData[p] || {}) : itemData; var raw = sectionRoot[subKey]; @@ -1004,7 +1008,7 @@ function removeSubRow(cardID, subKey, idx) { } arr.splice(idx, 1); sectionRoot[subKey] = arr; - renderCurrent(); + renderItemEditor(itemID, itemData); } // ========================================================================= @@ -1044,13 +1048,14 @@ function addSubSubRow(cardID, subKey, rowIdx, subSubKey) { function removeSubSubRow(cardID, subKey, rowIdx, subSubKey, subSubIdx) { var sec = SECTIONS.find(function(s) { return s.id === cardID; }); if (!sec) return; + ced_syncDOMToData(itemData); var p = cardPath(sec); var sectionRoot = p ? (itemData[p] || {}) : itemData; var arr = sectionRoot[subKey] || []; if (rowIdx >= arr.length) return; var subArr = arr[rowIdx][subSubKey] || []; subArr.splice(subSubIdx, 1); - renderCurrent(); + renderItemEditor(itemID, itemData); } // ========================================================================= diff --git a/internal/admin/static/mobeditor.js b/internal/admin/static/mobeditor.js index 73dd16b..debe7e0 100644 --- a/internal/admin/static/mobeditor.js +++ b/internal/admin/static/mobeditor.js @@ -149,7 +149,7 @@ function loadMob(id) { } function renderMobEditor(id, data) { - var html = '

Mob: ' + esc(id) + '

'; + var html = renderRenameableHeader('Mob', id); html += '
'; html += '
'; @@ -574,6 +574,7 @@ function toggleMobCardWidth(id) { function removeMobSection(id) { var sec = MOB_SECTIONS.find(function(s) { return s.id === id; }); if (!sec) return; + ced_syncDOMToData(mobData); if (sec.path) { delete mobData[sec.path]; } else { @@ -582,7 +583,7 @@ function removeMobSection(id) { if (sec.subtables) sec.subtables.forEach(function(st) { delete mobData[st.key]; }); } delete addedSections[id]; - renderCurrent(); + renderMobEditor(mobID, mobData); } function mobAddSection() { @@ -635,8 +636,9 @@ function addMobSubRow(cardID, subKey) { function removeMobSubRow(cardID, subKey, idx) { var sec = MOB_SECTIONS.find(function(s) { return s.id === cardID; }); if (!sec) return; + ced_syncDOMToData(mobData); ced_removeSubRow(sec, subKey, idx, mobData, mobCardPath); - renderCurrent(); + renderMobEditor(mobID, mobData); } function addMobLootRow(kind) { diff --git a/internal/admin/static/moduleeditor.js b/internal/admin/static/moduleeditor.js index f1fb0fd..5bcb7bc 100644 --- a/internal/admin/static/moduleeditor.js +++ b/internal/admin/static/moduleeditor.js @@ -67,7 +67,7 @@ function loadModuleEditor(id) { function loadItem(id) { loadModuleEditor(id); } function renderModuleEditor(id, data) { - var html = '

Module: ' + esc(id) + '

'; + var html = renderRenameableHeader('Module', id); html += '
'; html += '
'; @@ -187,11 +187,12 @@ function toggleCardWidth(id) { function removeSection(id) { var sec = MODULE_SECTIONS.find(function(s) { return s.id === id; }); if (!sec) return; + ced_syncDOMToData(moduleData); if (sec.path) { delete moduleData[sec.path]; } delete addedSections[id]; - renderCurrent(); + renderModuleEditor(moduleID, moduleData); } function addSection() { @@ -227,9 +228,10 @@ function addArrayItem(cardID) { function removeArrayItem(cardID, idx) { var sec = MODULE_SECTIONS.find(function(s) { return s.id === cardID; }); if (!sec || !sec.isArray) return; + ced_syncDOMToData(moduleData); var arr = moduleData[sec.path] || []; arr.splice(idx, 1); - renderCurrent(); + renderModuleEditor(moduleID, moduleData); } function saveModule() { diff --git a/internal/admin/static/objecteditor.js b/internal/admin/static/objecteditor.js index f8ef18a..5444985 100644 --- a/internal/admin/static/objecteditor.js +++ b/internal/admin/static/objecteditor.js @@ -157,7 +157,7 @@ function loadObject(id) { } function renderObjectEditor(id, data) { - var html = '

Object: ' + esc(id) + '

'; + var html = renderRenameableHeader('Object', id); html += '
'; html += '
'; @@ -729,12 +729,13 @@ function addToolRow(secID, key) { function removeToolRow(secID, key, idx) { var sec = SECTIONS.find(function(s) { return s.id === secID; }); if (!sec) return; + ced_syncDOMToData(objectData); var p = cardPath(sec); var arr = objectData[p] && objectData[p][key]; if (!arr) return; arr.splice(idx, 1); if (arr.length === 0) delete objectData[p][key]; - renderCurrent(); + renderObjectEditor(objectID, objectData); } function fetchToolTypes() { @@ -760,12 +761,13 @@ function toggleCardWidth(id) { function removeSection(id) { var sec = SECTIONS.find(function(s) { return s.id === id; }); if (!sec) return; + ced_syncDOMToData(objectData); if (sec.path) { delete objectData[sec.path]; } else if (id === 'steal') { delete objectData.steal; } - renderCurrent(); + renderObjectEditor(objectID, objectData); } function addSection() { @@ -812,9 +814,10 @@ function addArrayItem(cardID) { function removeArrayItem(cardID, idx) { var sec = SECTIONS.find(function(s) { return s.id === cardID; }); if (!sec || !sec.isArray) return; + ced_syncDOMToData(objectData); var arr = objectData[sec.path] || []; arr.splice(idx, 1); - renderCurrent(); + renderObjectEditor(objectID, objectData); } function addSubRow(cardID, subKey) { @@ -837,10 +840,11 @@ function addSubRow(cardID, subKey) { function removeSubRow(cardID, subKey, idx) { var sec = SECTIONS.find(function(s) { return s.id === cardID; }); if (!sec) return; + ced_syncDOMToData(objectData); var sectionRoot = objectData[sec.path || ''] || objectData; var arr = sectionRoot[subKey] || []; arr.splice(idx, 1); - renderCurrent(); + renderObjectEditor(objectID, objectData); } function addKVRow(cardID, subKey) { @@ -884,10 +888,11 @@ function hideUseInterField(idx, key) { window._useInterVis = window._useInterVis || {}; if (!window._useInterVis[idx]) window._useInterVis[idx] = {}; window._useInterVis[idx][key] = false; + ced_syncDOMToData(objectData); if (objectData.use_interactions && objectData.use_interactions[idx]) { objectData.use_interactions[idx][key] = ''; } - renderCurrent(); + renderObjectEditor(objectID, objectData); } function showCoreField(key) { @@ -899,6 +904,7 @@ function showCoreField(key) { function hideCoreField(key) { window._coreVis = window._coreVis || {}; window._coreVis[key] = false; + ced_syncDOMToData(objectData); if (key === 'aliases') { objectData.aliases = []; } else if (key === 'inroom_description') { @@ -906,7 +912,7 @@ function hideCoreField(key) { } else if (key === 'removal_item') { objectData.removal_item = ''; } - renderCurrent(); + renderObjectEditor(objectID, objectData); } function showGuardMob() { @@ -918,8 +924,9 @@ function showGuardMob() { function hideGuardMob() { window._stealVis = window._stealVis || {}; window._stealVis.guard_mob = false; + ced_syncDOMToData(objectData); if (objectData.steal) objectData.steal.guard_mob = ''; - renderCurrent(); + renderObjectEditor(objectID, objectData); } function toggleUseInterHelp() { diff --git a/internal/admin/static/techeditor.js b/internal/admin/static/techeditor.js index 70f881e..ffb2b7b 100644 --- a/internal/admin/static/techeditor.js +++ b/internal/admin/static/techeditor.js @@ -70,7 +70,7 @@ function loadTechEditor(id) { function loadItem(id) { loadTechEditor(id); } function renderTechEditor(id, data) { - var html = '

Tech: ' + esc(id) + '

'; + var html = renderRenameableHeader('Tech', id); html += '
'; html += '
'; -- cgit v1.2.3