aboutsummaryrefslogtreecommitdiff
path: root/internal/admin
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-07-13 20:36:26 -0400
committerhistoria <[not public]>2026-07-13 20:36:26 -0400
commit5ea48c1f91298b42924f6865ffc30810cceec2d3 (patch)
treef13ba8276e66493750b72124d75da8911311ff6f /internal/admin
parent223d198c95b64d3d803be5d914c5b899dd30c8eb (diff)
downloadthehouseoficarus-5ea48c1f91298b42924f6865ffc30810cceec2d3.tar.gz
feat(admin): triggers organized by directory, support duplicate and delete
Diffstat (limited to 'internal/admin')
-rw-r--r--internal/admin/api_triggers.go60
-rw-r--r--internal/admin/static/triggerseditor.js165
2 files changed, 154 insertions, 71 deletions
diff --git a/internal/admin/api_triggers.go b/internal/admin/api_triggers.go
index ccfd52d..749f5a1 100644
--- a/internal/admin/api_triggers.go
+++ b/internal/admin/api_triggers.go
@@ -13,23 +13,22 @@ import (
"thehouseoficarus/internal/behavior"
)
-type triggerEntry struct {
- ID string `json:"id"`
- Kind string `json:"kind"`
-}
-
func (s *AdminServer) handleTriggers(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
- entries, err := listTriggerEntries(s.dataDir)
+ tree, err := listYAMLTree(s.dataDir, "triggers")
if err != nil {
+ if os.IsNotExist(err) {
+ writeJSON(w, &YAMLTree{Root: []string{}, Dirs: map[string][]string{}})
+ return
+ }
writeJSON(w, map[string]any{"error": err.Error()})
return
}
- if entries == nil {
- entries = []triggerEntry{}
+ if tree == nil {
+ tree = &YAMLTree{Root: []string{}, Dirs: map[string][]string{}}
}
- writeJSON(w, entries)
+ writeJSON(w, tree)
case http.MethodPost:
var m map[string]any
if err := readJSON(r, &m); err != nil {
@@ -200,49 +199,6 @@ func kindToSubdir(kind string) string {
return "player_flag"
}
-func listTriggerEntries(dataDir string) ([]triggerEntry, error) {
- root := filepath.Join(dataDir, "triggers")
- if _, err := os.Stat(root); os.IsNotExist(err) {
- return []triggerEntry{}, nil
- }
- var entries []triggerEntry
- var walk func(p string) error
- walk = func(p string) error {
- dirs, err := os.ReadDir(p)
- if err != nil {
- return err
- }
- for _, entry := range dirs {
- full := filepath.Join(p, entry.Name())
- if entry.IsDir() {
- if err := walk(full); err != nil {
- return err
- }
- continue
- }
- ext := filepath.Ext(entry.Name())
- if ext != ".yaml" && ext != ".yml" {
- continue
- }
- id := strings.TrimSuffix(entry.Name(), ext)
- // Determine kind from the parent directory name.
- parent := filepath.Base(filepath.Dir(full))
- kind := ""
- if parent == "global_flag" {
- kind = "global"
- } else if parent == "player_flag" {
- kind = "player"
- }
- entries = append(entries, triggerEntry{ID: id, Kind: kind})
- }
- return nil
- }
- if err := walk(root); err != nil {
- return nil, err
- }
- return entries, nil
-}
-
// findTriggerFile searches for a trigger file in the triggers directory tree
// and returns its full path and the parent directory name (relative to the
// triggers root). Searches player_flag/ then global_flag/ then root.
diff --git a/internal/admin/static/triggerseditor.js b/internal/admin/static/triggerseditor.js
index 7cb5d9e..0c18ce4 100644
--- a/internal/admin/static/triggerseditor.js
+++ b/internal/admin/static/triggerseditor.js
@@ -19,31 +19,80 @@ function initTriggerEditor() {
}
function loadTriggerList() {
- API.get('/api/triggers').then(function(data) {
- allIDs = (data || []).map(function(e) { return e.id || e; });
- window._triggerEntries = data || [];
- renderTriggerList('');
- }).catch(function(e) {
- notify('Failed to load list: ' + e.message, 'error');
- });
+ loadList();
}
function renderTriggerList(filter) {
+ renderList(filter);
+}
+
+function renderList(filter) {
var el = $('#listEntries');
var f = (filter || '').toLowerCase();
- var entries = window._triggerEntries || [];
+ var t = treeData;
+
+ function filterIDs(ids) {
+ if (!f) return ids;
+ return ids.filter(function(id) { return String(id).toLowerCase().indexOf(f) >= 0; });
+ }
+
+ function renderItems(ids) {
+ var h = '';
+ (ids || []).forEach(function(id) {
+ h += '<div class="item' + (triggerID === id ? ' active' : '') + '" onclick="loadTriggerItem(\'' + escAttr(id) + '\')" oncontextmenu="showItemContextMenu(event,\'' + escAttr(id) + '\')">' + esc(id) + '</div>';
+ });
+ return h;
+ }
+
var html = '';
- entries.forEach(function(entry) {
- var id = entry.id || entry;
- if (f && id.toLowerCase().indexOf(f) === -1) return;
- var badge = entry.kind === 'player' ? ' [Player]' : (entry.kind === 'global' ? ' [Global]' : '');
- var badgeClass = entry.kind === 'player' ? 'dc-item' : (entry.kind === 'global' ? 'dc-item' : '');
- html += '<div class="item' + (triggerID === id ? ' active' : '') + '" onclick="loadTriggerItem(\'' + escAttr(id) + '\')">';
- html += '<span class="dc-id">' + esc(id) + '</span>';
- html += '<span class="dc-name" style="color:' + (entry.kind === 'player' ? '#6af' : '#e8c47c') + '">' + esc(badge) + '</span>';
- html += '</div>';
- });
+ var hasDirs = Object.keys(t.dirs).length > 0;
+
+ if (t.root.length > 0 || !hasDirs) {
+ if (hasDirs) {
+ var filtered = filterIDs(t.root);
+ if (filtered.length > 0) {
+ var open = !!f || dirOpenState['Root'] === true;
+ html += '<details class="dir-group" data-name="Root"' + (open ? ' open' : '') + '>';
+ html += '<summary class="dir-header">';
+ html += '<span class="dir-arrow"></span>';
+ html += '<span class="dir-name">Root</span>';
+ html += '<span class="dir-count">' + (f ? filtered.length + '/' + t.root.length : t.root.length) + '</span>';
+ html += '</summary>';
+ html += '<div class="dir-items">' + renderItems(filtered) + '</div>';
+ html += '</details>';
+ }
+ } else {
+ html += renderItems(filterIDs(t.root));
+ }
+ }
+
+ if (hasDirs) {
+ Object.keys(t.dirs).sort().forEach(function(dir) {
+ var ids = t.dirs[dir] || [];
+ if (ids.length === 0 && !f) return;
+ var filtered = filterIDs(ids);
+ if (f && filtered.length === 0) return;
+
+ var open = !!f || dirOpenState[dir] === true;
+ html += '<details class="dir-group" data-name="' + escAttr(dir) + '"' + (open ? ' open' : '') + '>';
+ html += '<summary class="dir-header">';
+ html += '<span class="dir-arrow"></span>';
+ html += '<span class="dir-name">' + esc(dir) + '</span>';
+ html += '<span class="dir-count">' + (f ? filtered.length + '/' + ids.length : ids.length) + '</span>';
+ html += '</summary>';
+ html += '<div class="dir-items">' + renderItems(filtered) + '</div>';
+ html += '</details>';
+ });
+ }
+
el.innerHTML = html || '<p style="color:#888;text-align:center;margin-top:20px;font-size:11px">No triggers</p>';
+
+ el.querySelectorAll('details.dir-group').forEach(function(d) {
+ d.addEventListener('toggle', function() {
+ var n = d.getAttribute('data-name');
+ if (n) dirOpenState[n] = d.open;
+ });
+ });
}
function loadTriggerItem(id) {
@@ -170,7 +219,8 @@ function renderTriggerEditor(id, data) {
html += '<div class="btn-row">';
html += '<button class="btn btn-primary" onclick="saveTrigger()">Save</button>';
- html += '<button class="btn" style="margin-left:auto" onclick="deleteTrigger()">Remove</button>';
+ html += '<button class="btn" onclick="duplicateTrigger()">Duplicate</button>';
+ html += '<button class="btn btn-danger" onclick="deleteTrigger()">Delete Trigger</button>';
html += '</div>';
$('#editorMain').innerHTML = html;
@@ -214,6 +264,83 @@ function saveTrigger() {
}).catch(function(e) { notify('Save failed: ' + e.message, 'error'); });
}
+function duplicateTrigger(sourceID) {
+ var src = sourceID || triggerID;
+ if (!src) return;
+
+ var done = function(data) {
+ var allTriggerIDs = (treeData.root || []).slice();
+ var dirs = treeData.dirs || {};
+ Object.keys(dirs).forEach(function(d) {
+ allTriggerIDs = allTriggerIDs.concat(dirs[d] || []);
+ });
+
+ var baseID = src + '_copy';
+ var newID = baseID;
+ var counter = 2;
+ while (allTriggerIDs.indexOf(newID) >= 0) {
+ newID = baseID + '_' + counter;
+ counter++;
+ }
+
+ var copy = JSON.parse(JSON.stringify(data));
+ delete copy._raw;
+ delete copy._path;
+ delete copy._kind;
+ copy.id = newID;
+ ced_cleanOutput(copy);
+
+ API.post('/api/triggers', {id: newID, on_player_flag: '', steps: []}).then(function() {
+ return API.put('/api/triggers/' + encodeURIComponent(newID), copy);
+ }).then(function() {
+ notify('Duplicated as ' + newID, 'success');
+ updateUndoBar();
+ loadTriggerList();
+ loadTrigger(newID);
+ }).catch(function(e) { notify('Duplicate failed: ' + e.message, 'error'); });
+ };
+
+ if (sourceID) {
+ API.get('/api/triggers/' + encodeURIComponent(sourceID)).then(done).catch(function(e) { notify('Duplicate failed: ' + e.message, 'error'); });
+ } else {
+ var flagTypeEl = document.querySelector('input[name="trigFlagType"]:checked');
+ if (flagTypeEl) {
+ delete triggerData.on_player_flag;
+ delete triggerData.on_global_flag;
+ var n = $('#trigFlagName');
+ if (flagTypeEl.value === 'player') triggerData.on_player_flag = n ? n.value.trim() : '';
+ else triggerData.on_global_flag = n ? n.value.trim() : '';
+ }
+ window._ieSyncAll(window._editorData);
+ mergeTriggerFromEditor();
+ done(triggerData);
+ }
+}
+
+function showItemContextMenu(e, id) {
+ var items = [
+ {label: 'Duplicate', handler: function() { duplicateTrigger(id); }},
+ {label: 'Delete Trigger', cls: 'danger', handler: function() { deleteTriggerByID(id); }},
+ ];
+ openContextMenu(e, items);
+}
+
+function deleteTriggerByID(id) {
+ if (!confirm('Delete trigger "' + id + '"?')) return;
+ API.del('/api/triggers/' + encodeURIComponent(id)).then(function() {
+ notify('Trigger ' + id + ' deleted', 'success');
+ updateUndoBar();
+ if (triggerID === id) {
+ triggerID = null;
+ currentID = null;
+ triggerData = null;
+ triggerKind = '';
+ $('#editorMain').innerHTML = '<p style="color:#888;text-align:center;margin-top:60px">Deleted</p>';
+ }
+ loadTriggerList();
+ }).catch(function(e) { notify('Delete failed: ' + e.message, 'error'); });
+}
+
function deleteTrigger() {
if (!confirm('Delete trigger "' + triggerID + '"?')) return;
API.del('/api/triggers/' + encodeURIComponent(triggerID)).then(function() {