diff options
| author | historia <[not public]> | 2026-07-05 18:52:51 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-07-05 18:52:51 -0400 |
| commit | 6bd5365cc22f84c047c1585e3f54571af46f53e5 (patch) | |
| tree | 72df0934c72baa1e31817d7db999832c6efb115f /internal | |
| parent | 9b54188970c51070d86f4c87e909ea7836c86f07 (diff) | |
| download | thehouseoficarus-6bd5365cc22f84c047c1585e3f54571af46f53e5.tar.gz | |
feat: renameable directories and files in admin gui
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/admin/server.go | 75 | ||||
| -rw-r--r-- | internal/admin/static/admin.css | 11 | ||||
| -rw-r--r-- | internal/admin/static/admin.js | 65 | ||||
| -rw-r--r-- | internal/admin/static/cardeditor.js | 6 | ||||
| -rw-r--r-- | internal/admin/static/dropeditor.js | 5 | ||||
| -rw-r--r-- | internal/admin/static/editor.js | 108 | ||||
| -rw-r--r-- | internal/admin/static/hazardeditor.js | 5 | ||||
| -rw-r--r-- | internal/admin/static/itemeditor.js | 17 | ||||
| -rw-r--r-- | internal/admin/static/mobeditor.js | 8 | ||||
| -rw-r--r-- | internal/admin/static/moduleeditor.js | 8 | ||||
| -rw-r--r-- | internal/admin/static/objecteditor.js | 23 | ||||
| -rw-r--r-- | internal/admin/static/techeditor.js | 2 | ||||
| -rw-r--r-- | internal/admin/undo.go | 3 | ||||
| -rw-r--r-- | internal/admin/yaml_util.go | 62 |
14 files changed, 357 insertions, 41 deletions
diff --git a/internal/admin/server.go b/internal/admin/server.go index 53dd5c8..e29de9a 100644 --- a/internal/admin/server.go +++ b/internal/admin/server.go @@ -117,30 +117,37 @@ func NewServer(cfg *config.Config, useTLS bool, accountStore *player.AccountStor apiMux.HandleFunc("/api/rooms", s.handleRooms) apiMux.HandleFunc("/api/rooms/new-empty", s.handleCreateEmptyRoom) apiMux.HandleFunc("/api/rooms/", s.handleRoomByID) + apiMux.HandleFunc("/api/objects/rename", s.handleEntityRename("objects")) apiMux.HandleFunc("/api/objects/move", s.handleEntityMove("objects")) apiMux.HandleFunc("/api/objects/dirs", s.handleEntityDirs("objects")) apiMux.HandleFunc("/api/objects", s.handleObjects) apiMux.HandleFunc("/api/objects/", s.handleObjectByID) + apiMux.HandleFunc("/api/items/rename", s.handleEntityRename("items")) apiMux.HandleFunc("/api/items/move", s.handleEntityMove("items")) apiMux.HandleFunc("/api/items/dirs", s.handleEntityDirs("items")) apiMux.HandleFunc("/api/items", s.handleItems) apiMux.HandleFunc("/api/items/", s.handleItemByID) + apiMux.HandleFunc("/api/mobs/rename", s.handleEntityRename("mobs")) apiMux.HandleFunc("/api/mobs/move", s.handleEntityMove("mobs")) apiMux.HandleFunc("/api/mobs/dirs", s.handleEntityDirs("mobs")) apiMux.HandleFunc("/api/mobs", s.handleMobs) apiMux.HandleFunc("/api/mobs/", s.handleMobByID) + apiMux.HandleFunc("/api/drops/rename", s.handleEntityRename("drops")) apiMux.HandleFunc("/api/drops/move", s.handleEntityMove("drops")) apiMux.HandleFunc("/api/drops/dirs", s.handleEntityDirs("drops")) apiMux.HandleFunc("/api/drops", s.handleDrops) apiMux.HandleFunc("/api/drops/", s.handleDropByID) + apiMux.HandleFunc("/api/hazards/rename", s.handleEntityRename("hazards")) apiMux.HandleFunc("/api/hazards/move", s.handleEntityMove("hazards")) apiMux.HandleFunc("/api/hazards/dirs", s.handleEntityDirs("hazards")) apiMux.HandleFunc("/api/hazards", s.handleHazards) apiMux.HandleFunc("/api/hazards/", s.handleHazardByID) + apiMux.HandleFunc("/api/techs/rename", s.handleEntityRename("techs")) apiMux.HandleFunc("/api/techs/move", s.handleEntityMove("techs")) apiMux.HandleFunc("/api/techs/dirs", s.handleEntityDirs("techs")) apiMux.HandleFunc("/api/techs", s.handleTechs) apiMux.HandleFunc("/api/techs/", s.handleTechByID) + apiMux.HandleFunc("/api/modules/rename", s.handleEntityRename("modules")) apiMux.HandleFunc("/api/modules/move", s.handleEntityMove("modules")) apiMux.HandleFunc("/api/modules/dirs", s.handleEntityDirs("modules")) apiMux.HandleFunc("/api/modules", s.handleModules) @@ -481,9 +488,10 @@ func (s *AdminServer) handleEntityDirs(entityType string) http.HandlerFunc { return } var body struct { - Action string `json:"action"` - Name string `json:"name"` - Dir string `json:"dir"` + Action string `json:"action"` + Name string `json:"name"` + Dir string `json:"dir"` + NewName string `json:"new_name"` } if err := readJSON(r, &body); err != nil { writeJSON(w, map[string]any{"error": "invalid"}) @@ -506,12 +514,50 @@ func (s *AdminServer) handleEntityDirs(entityType string) http.HandlerFunc { s.undoStack.Push(ch) } writeJSON(w, map[string]any{"ok": true, "moved": len(changes) - 1}) + case "rename_dir": + if err := renameEntityDir(s.dataDir, entityType, body.Dir, body.NewName); err != nil { + writeJSON(w, map[string]any{"error": err.Error()}) + return + } + writeJSON(w, map[string]any{"ok": true, "name": body.NewName}) default: writeJSON(w, map[string]any{"error": "unknown action"}) } } } +func (s *AdminServer) handleEntityRename(entityType string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + var body struct { + ID string `json:"id"` + NewID string `json:"new_id"` + } + if err := readJSON(r, &body); err != nil || body.ID == "" || body.NewID == "" { + writeJSON(w, map[string]any{"error": "invalid"}) + return + } + oldPath, newPath, data, err := renameEntityFile(s.dataDir, entityType, body.ID, body.NewID) + if err != nil { + writeJSON(w, map[string]any{"error": err.Error()}) + return + } + desc := fmt.Sprintf("rename %s %s to %s", entityType, body.ID, body.NewID) + s.undoStack.Push(ChangeDesc{ + Description: desc, + FilePath: oldPath, + NewFilePath: newPath, + OldContent: data, + NewContent: data, + }) + s.invalidateEntityCaches(entityType) + writeJSON(w, map[string]any{"ok": true, "id": body.NewID}) + } +} + func (s *AdminServer) handleEntityMove(entityType string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { @@ -542,6 +588,7 @@ func (s *AdminServer) handleEntityMove(entityType string) http.HandlerFunc { OldContent: data, NewContent: data, }) + s.invalidateEntityCaches(entityType) writeJSON(w, map[string]any{"ok": true}) } } @@ -565,6 +612,28 @@ func (s *AdminServer) rebuildAfterUndo(change *ChangeDesc) { } } } + + for _, p := range changeFiles(change) { + s.invalidateEntityCacheForPath(p) + } +} + +func (s *AdminServer) invalidateEntityCaches(entityType string) { + switch entityType { + case "mobs": + s.mobStore.ReloadDefs(s.dataDir) + case "drops": + behavior.ClearDropIndex() + } +} + +func (s *AdminServer) invalidateEntityCacheForPath(path string) { + for _, subdir := range []string{"mobs", "drops"} { + rel, err := filepath.Rel(filepath.Join(s.dataDir, subdir), path) + if err == nil && !strings.HasPrefix(rel, "..") && rel != "." { + s.invalidateEntityCaches(subdir) + } + } } func changeFiles(change *ChangeDesc) []string { 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 = '<div class="rename-header">'; + h += '<h2 class="rename-header-text">' + esc(label) + ': ' + esc(id) + '</h2>'; + h += '<span class="rename-pencil" onclick="startHeaderRename(this,\'' + escAttr(id) + '\')" title="Rename">\u270E</span>'; + h += '<div class="rename-edit-row" style="display:none"></div>'; + h += '</div>'; + 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 = '<input class="rename-input" value="' + escAttr(oldID) + '" maxlength="128">' + + '<button class="btn btn-sm rename-btn-confirm" title="Confirm">\u2713</button>' + + '<button class="btn btn-sm rename-btn-cancel" title="Cancel">\u2717</button>'; + + 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 = '<h2>Drop Table: ' + esc(id) + '</h2>'; + var html = renderRenameableHeader('Drop Table', id); html += '<div class="tabs"><button class="tab active" onclick="switchTab(event,\'fields\')">Fields</button>'; html += '<button class="tab" onclick="switchTab(event,\'yaml\')">Raw YAML</button></div>'; @@ -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 += '<div class="dir-items"' + (name && !expanded ? ' style="display:none"' : '') + ' ondragover="dirDragOver(event,\'' + (name || '') + '\')" ondragleave="dirDragLeave(event)" ondrop="dirDrop(event,\'' + (name || '') + '\')">'; displayIds.forEach(function(id) { - h += '<div class="item' + (id === currentID ? ' active' : '') + '" onclick="loadItem(\'' + esc(id) + '\')" draggable="true" ondragstart="itemDragStart(event,\'' + escAttr(id) + '\')">' + esc(id) + '</div>'; + h += '<div class="item' + (id === currentID ? ' active' : '') + '" onclick="loadItem(\'' + esc(id) + '\')" draggable="true" ondragstart="itemDragStart(event,\'' + escAttr(id) + '\')" oncontextmenu="showItemContextMenu(event,\'' + escAttr(id) + '\')">' + esc(id) + '</div>'; }); h += '</div>'; h += '</div>'; @@ -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 = '<input class="item-rename-input" value="' + escAttr(id) + '" maxlength="128">'; + 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 = '<h2>Hazard: ' + esc(id) + '</h2>'; + var html = renderRenameableHeader('Hazard', id); html += '<div class="tabs"><button class="tab active" onclick="switchTab(event,\'fields\')">Fields</button>'; html += '<button class="tab" onclick="switchTab(event,\'yaml\')">Raw YAML</button></div>'; @@ -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 = '<h2>Item: ' + esc(id) + '</h2>'; + var html = renderRenameableHeader('Item', id); html += '<div class="tabs"><button class="tab active" onclick="switchTab(event,\'fields\')">Fields</button>'; html += '<button class="tab" onclick="switchTab(event,\'yaml\')">Raw YAML</button></div>'; @@ -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 = '<h2>Mob: ' + esc(id) + '</h2>'; + var html = renderRenameableHeader('Mob', id); html += '<div class="tabs"><button class="tab active" onclick="switchTab(event,\'fields\')">Fields</button>'; html += '<button class="tab" onclick="switchTab(event,\'yaml\')">Raw YAML</button></div>'; @@ -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 = '<h2>Module: ' + esc(id) + '</h2>'; + var html = renderRenameableHeader('Module', id); html += '<div class="tabs"><button class="tab active" onclick="switchTab(event,\'fields\')">Fields</button>'; html += '<button class="tab" onclick="switchTab(event,\'yaml\')">Raw YAML</button></div>'; @@ -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 = '<h2>Object: ' + esc(id) + '</h2>'; + var html = renderRenameableHeader('Object', id); html += '<div class="tabs"><button class="tab active" onclick="switchTab(event,\'fields\')">Fields</button>'; html += '<button class="tab" onclick="switchTab(event,\'yaml\')">Raw YAML</button></div>'; @@ -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 = '<h2>Tech: ' + esc(id) + '</h2>'; + var html = renderRenameableHeader('Tech', id); html += '<div class="tabs"><button class="tab active" onclick="switchTab(event,\'fields\')">Fields</button>'; html += '<button class="tab" onclick="switchTab(event,\'yaml\')">Raw YAML</button></div>'; diff --git a/internal/admin/undo.go b/internal/admin/undo.go index 0c5ebaa..4c39ec6 100644 --- a/internal/admin/undo.go +++ b/internal/admin/undo.go @@ -35,6 +35,9 @@ func (c ChangeDesc) ShortDesc() string { case c.IsDelete: return fmt.Sprintf("Deleted %s", filepath.Base(c.FilePath)) case c.NewFilePath != "": + if filepath.Dir(c.FilePath) == filepath.Dir(c.NewFilePath) { + return fmt.Sprintf("Renamed %s", filepath.Base(c.NewFilePath)) + } return fmt.Sprintf("Moved %s", filepath.Base(c.NewFilePath)) default: return fmt.Sprintf("Edited %s", filepath.Base(c.FilePath)) diff --git a/internal/admin/yaml_util.go b/internal/admin/yaml_util.go index 519a04c..b869ffe 100644 --- a/internal/admin/yaml_util.go +++ b/internal/admin/yaml_util.go @@ -214,6 +214,52 @@ func createEntityDir(dataDir, subdir, name string) error { return os.Mkdir(dirPath, 0755) } +func renameEntityFile(dataDir, subdir, oldID, newID string) (string, string, []byte, error) { + oldPath, err := findEntityPath(dataDir, subdir, oldID) + if err != nil { + return "", "", nil, fmt.Errorf("entity not found: %s/%s", subdir, oldID) + } + dir := filepath.Dir(oldPath) + newPath := filepath.Join(dir, newID+".yaml") + if oldPath == newPath { + return "", "", nil, fmt.Errorf("new name is same as current") + } + if _, err := os.Stat(newPath); err == nil { + return "", "", nil, fmt.Errorf("entity %s already exists", newID) + } + data, err := os.ReadFile(oldPath) + if err != nil { + return "", "", nil, fmt.Errorf("read: %w", err) + } + if err := os.WriteFile(newPath, data, 0644); err != nil { + return "", "", nil, fmt.Errorf("write: %w", err) + } + if err := os.Remove(oldPath); err != nil { + return "", "", nil, fmt.Errorf("remove old: %w", err) + } + return oldPath, newPath, data, nil +} + +func renameEntityDir(dataDir, subdir, oldName, newName string) error { + oldName = strings.TrimSpace(oldName) + newName = strings.TrimSpace(newName) + if oldName == "" || newName == "" || strings.ContainsAny(oldName, "/\\") || oldName == "." || oldName == ".." { + return fmt.Errorf("invalid old directory name") + } + if strings.ContainsAny(newName, "/\\") || newName == "." || newName == ".." { + return fmt.Errorf("invalid new directory name") + } + oldPath := filepath.Join(dataDir, subdir, oldName) + if _, err := os.Stat(oldPath); os.IsNotExist(err) { + return fmt.Errorf("directory %s does not exist", oldName) + } + newPath := filepath.Join(dataDir, subdir, newName) + if _, err := os.Stat(newPath); err == nil { + return fmt.Errorf("directory %s already exists", newName) + } + return os.Rename(oldPath, newPath) +} + func deleteEntityDir(dataDir, subdir, dir string) ([]ChangeDesc, error) { dir = strings.TrimSpace(dir) if dir == "" || strings.ContainsAny(dir, "/\\") || dir == "." || dir == ".." { @@ -225,13 +271,27 @@ func deleteEntityDir(dataDir, subdir, dir string) ([]ChangeDesc, error) { return nil, fmt.Errorf("directory not found: %s", dir) } + rootDir := filepath.Join(dataDir, subdir) + var clashes []string + for _, e := range entries { + if e.IsDir() || filepath.Ext(e.Name()) != ".yaml" { + continue + } + if _, err := os.Stat(filepath.Join(rootDir, e.Name())); err == nil { + clashes = append(clashes, e.Name()) + } + } + if len(clashes) > 0 { + return nil, fmt.Errorf("files already exist in root: %s", strings.Join(clashes, ", ")) + } + var changes []ChangeDesc for _, e := range entries { if e.IsDir() || filepath.Ext(e.Name()) != ".yaml" { continue } oldPath := filepath.Join(srcDir, e.Name()) - newPath := filepath.Join(dataDir, subdir, e.Name()) + newPath := filepath.Join(rootDir, e.Name()) data, err := os.ReadFile(oldPath) if err != nil { return nil, fmt.Errorf("read %s: %w", e.Name(), err) |
