left4me/l4d2web/templates/overlay_detail.html
mwiegand 2bba1f31d0
fix(files-overlay): post-deploy bug sweep + root-as-row UX
Three bugs surfaced in browser testing, plus one UX request:

1. The Uploads panel and the binary-mode editor sub-panels stayed
   visible after `el.hidden = true` because their `display: flex/grid`
   rules in components.css have the same specificity as the UA's
   `[hidden]{display:none}` and come later in cascade. Add a targeted
   `[hidden]!important` rule for the affected classes.

2. Clicking a folder toggle inside a `files` overlay did nothing.
   `file-tree.js` looked for `.file-tree-children` via
   `button.nextElementSibling`, but the files-overlay row template
   inserts a per-row action span between the toggle and the children
   div. Switch to `closest('.file-tree-row').querySelector(':scope >
   .file-tree-children')` so both row variants resolve correctly.

3. Pressing Enter on the new-folder dialog did nothing — the keydown
   handler was attached with `{once:true}` inside `openNewFolder`,
   so the first letter the user typed consumed the listener and Enter
   never fired. Move the listener to module init so it survives
   subsequent keystrokes and dialog reopenings.

UX: render the overlay root as a row inside the tree (label
"(overlay root)") rather than as a separate toolbar. The root row
carries the same `+ new file · + new folder · ⬇ zip` hover-action
column as every other folder row, so drop-on-row, hover-reveal, and
data-target-path semantics are uniform across the tree.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 19:46:19 +02:00

272 lines
12 KiB
HTML

{% extends "base.html" %}
{% block title %}Overlay {{ overlay.name }} | left4me{% endblock %}
{% block content %}
{% set can_edit = g.user.admin or (overlay.type in ['workshop', 'script', 'files'] and overlay.user_id == g.user.id) %}
{% set is_files_overlay = overlay.type == 'files' %}
{% set files_can_edit = is_files_overlay and can_edit %}
<section class="panel">
<div class="page-heading">
<h1>Overlay: {{ overlay.name }}</h1>
</div>
<dl class="server-info">
<div><dt>Type</dt><dd>{{ overlay.type }}</dd></div>
<div><dt>Scope</dt><dd>{% if overlay.user_id %}private{% else %}system{% endif %}</dd></div>
</dl>
{% if overlay.type == 'script' %}
<h2 class="section-title">Script</h2>
{% if can_edit %}
<form method="post" action="/overlays/{{ overlay.id }}/script" class="stack">
<input type="hidden" name="csrf_token" value="{{ session.get('csrf_token', '') }}">
<label>Bash script
<textarea name="script" rows="20" spellcheck="false">{{ overlay.script or "" }}</textarea>
</label>
<p class="muted">Runs sandboxed against the overlay directory mounted at <code>/overlay</code>.</p>
{% if not latest_build_is_running %}
<div class="form-actions-inline">
<button type="submit" name="action" value="save_build">Save and build</button>
<button type="submit" name="action" value="save_reset_build">Save, reset and rebuild</button>
</div>
{% endif %}
</form>
{% else %}
<pre class="script-preview">{{ overlay.script or "" }}</pre>
{% endif %}
{% include "_overlay_build_status.html" %}
{% endif %}
{% if overlay.type == 'workshop' %}
<h2 class="section-title">Workshop items</h2>
{% if can_edit and not latest_build_is_running %}
<form method="post" action="/overlays/{{ overlay.id }}/items" class="stack">
<input type="hidden" name="csrf_token" value="{{ session.get('csrf_token', '') }}">
<fieldset class="workshop-input-mode">
<legend>Input mode</legend>
<label><input type="radio" name="input_mode" value="items" checked> Items (paste IDs or URLs; one or many)</label>
<label><input type="radio" name="input_mode" value="collection"> Collection (one ID or URL)</label>
</fieldset>
<label>Workshop input <textarea name="input" rows="3" placeholder="123456789&#10;https://steamcommunity.com/sharedfiles/filedetails/?id=987654321"></textarea></label>
<button type="submit">Add</button>
</form>
{% endif %}
<div id="overlay-item-table">
{% include "_overlay_item_table.html" with context %}
</div>
{% include "_overlay_build_status.html" %}
{% endif %}
<h2 class="section-title">Files</h2>
{% if files_can_edit %}
<div class="files-manager"
data-overlay-id="{{ overlay.id }}"
data-base-url="/overlays/{{ overlay.id }}">
<p class="muted files-manager-hint">Drop files or folders onto a folder row to upload. Drag rows inside the tree to move them.</p>
<ul class="file-tree files-tree-root" data-files-overlay="1">
<li class="file-tree-row file-tree-row-dir files-row files-row-root"
data-target-path=""
data-row-kind="dir">
<span class="files-row-root-label">(overlay root)</span>
<span class="files-row-actions" aria-label="Overlay root actions">
<button type="button" class="files-row-action" data-action="new-file" data-target-path="">+ new file</button>
<button type="button" class="files-row-action" data-action="new-folder" data-target-path="">+ new folder</button>
<button type="button" class="files-row-action" data-action="zip" data-target-path="">⬇ zip</button>
</span>
<div class="file-tree-children files-root-children">
{% if not file_tree_root_entries %}
<p class="muted files-empty">Empty — drop files here, or click "+ new file" on this row.</p>
{% else %}
{% set entries = file_tree_root_entries %}
{% set truncated = file_tree_truncated %}
{% set truncated_count = file_tree_truncated_count %}
{% set files_base_url = "/overlays/" ~ overlay.id %}
{% set download_supported = True %}
{% set files_overlay = True %}
{% include "_overlay_file_tree.html" %}
{% endif %}
</div>
</li>
</ul>
</div>
{% else %}
{% if not file_tree_root_entries %}
<p class="muted">No files yet — build this overlay to populate it.</p>
{% else %}
{% set entries = file_tree_root_entries %}
{% set truncated = file_tree_truncated %}
{% set truncated_count = file_tree_truncated_count %}
{% set files_base_url = "/overlays/" ~ overlay.id %}
{% set download_supported = True %}
{% include "_overlay_file_tree.html" %}
{% endif %}
{% endif %}
<h2 class="section-title">Used by</h2>
{% if using_blueprints %}
<ul class="used-by-list">
{% for blueprint in using_blueprints %}
<li><a href="/blueprints/{{ blueprint.id }}">{{ blueprint.name }}</a></li>
{% endfor %}
</ul>
{% else %}
<p class="muted">Not used by any blueprint.</p>
{% endif %}
</section>
{% if can_edit %}
<div class="page-footer-actions">
<button type="button" class="danger-outline" data-modal-open="delete-overlay-modal">Delete overlay</button>
<a href="#" class="link-button" data-modal-open="rename-overlay-modal">Rename</a>
</div>
<dialog id="rename-overlay-modal" class="modal" aria-labelledby="rename-overlay-title">
<div class="modal-header">
<h2 id="rename-overlay-title">Rename overlay</h2>
<button type="button" class="modal-close" data-modal-close aria-label="Close">&times;</button>
</div>
<div class="modal-body">
<form method="post" action="/overlays/{{ overlay.id }}" class="inline-save">
<input type="hidden" name="csrf_token" value="{{ session.get('csrf_token', '') }}">
<input name="name" value="{{ overlay.name }}" required autofocus>
<button type="submit">Save</button>
</form>
</div>
</dialog>
<dialog id="delete-overlay-modal" class="modal" aria-labelledby="delete-overlay-title">
<div class="modal-header">
<h2 id="delete-overlay-title">Delete overlay "{{ overlay.name }}"?</h2>
<button type="button" class="modal-close" data-modal-close aria-label="Close">&times;</button>
</div>
<div class="modal-body">
<p>This cannot be undone. Overlays in use by a blueprint cannot be deleted.</p>
</div>
<div class="modal-footer">
<button type="button" class="button-secondary" data-modal-close>Cancel</button>
<form method="post" action="/overlays/{{ overlay.id }}/delete" class="inline-form">
<input type="hidden" name="csrf_token" value="{{ session.get('csrf_token', '') }}">
<button class="danger" type="submit">Delete</button>
</form>
</div>
</dialog>
{% endif %}
{% if files_can_edit %}
<dialog id="files-editor-modal" class="modal modal-wide" aria-labelledby="files-editor-title">
<div class="modal-header">
<h2 id="files-editor-title" class="files-editor-path"><span class="files-editor-title-text"></span></h2>
<button type="button" class="modal-close" data-modal-close aria-label="Close">&times;</button>
</div>
<div class="modal-body">
<label class="files-editor-field">
<span class="files-field-label">Filename</span>
<input type="text" class="files-editor-filename" autocomplete="off" spellcheck="false">
</label>
<p class="files-editor-rename-hint" hidden>↻ Save will rename <code class="files-rename-from"></code><code class="files-rename-to"></code>.</p>
<div class="files-editor-text">
<label class="files-editor-field">
<span class="files-field-label">Content</span>
<textarea class="files-editor-content" rows="14" spellcheck="false"></textarea>
</label>
<div class="files-editor-meta muted">
<span class="files-editor-byte-count">UTF-8 · 0 bytes</span>
<span>Ctrl+S to save</span>
</div>
</div>
<div class="files-editor-binary" hidden>
<div class="files-editor-binary-note">
<strong>⛌ Inline editing not available</strong>
· <span class="files-editor-binary-size"></span> · binary content
</div>
<label class="files-field-label files-editor-binary-replace-label">Replace file</label>
<div class="files-editor-replace-zone">
<p class="files-editor-replace-idle">↑ Drop a file here to replace ·
<button type="button" class="link-button files-editor-replace-browse">browse</button> ·
single file only · keeps the filename
</p>
<p class="files-editor-replace-queued" hidden>
<strong class="files-editor-replace-name"></strong> ·
<span class="files-editor-replace-size"></span> ·
<span class="muted">queued</span>
<button type="button" class="link-button files-editor-replace-clear" aria-label="Clear queued replacement"></button>
</p>
<input type="file" class="files-editor-replace-input" hidden>
</div>
</div>
</div>
<div class="modal-footer files-editor-footer">
<button type="button" class="danger-outline files-editor-delete">Delete</button>
<span class="files-editor-footer-spacer"></span>
<a class="button-secondary files-editor-download" href="#" hidden>⬇ Download</a>
<button type="button" class="button-secondary" data-modal-close>Cancel</button>
<button type="button" class="files-editor-save">Save</button>
</div>
</dialog>
<dialog id="files-new-folder-modal" class="modal" aria-labelledby="files-new-folder-title">
<div class="modal-header">
<h2 id="files-new-folder-title">New folder in <code class="files-new-folder-target"></code></h2>
<button type="button" class="modal-close" data-modal-close aria-label="Close">&times;</button>
</div>
<div class="modal-body">
<label class="files-editor-field">
<span class="files-field-label">Folder name</span>
<input type="text" class="files-new-folder-name" autocomplete="off" spellcheck="false" placeholder="e.g. sourcemod or sourcemod/configs">
</label>
<p class="muted">Slashes create nested folders in one go.</p>
<p class="files-new-folder-error" hidden></p>
</div>
<div class="modal-footer">
<button type="button" class="button-secondary" data-modal-close>Cancel</button>
<button type="button" class="files-new-folder-create">Create</button>
</div>
</dialog>
<dialog id="files-conflict-modal" class="modal" aria-labelledby="files-conflict-title">
<div class="modal-header">
<h2 id="files-conflict-title">File already exists</h2>
<button type="button" class="modal-close" data-modal-close aria-label="Close">&times;</button>
</div>
<div class="modal-body">
<p>A file already exists at <code class="files-conflict-path"></code>.</p>
<p class="muted">Choose how to handle this upload.</p>
</div>
<div class="modal-footer">
<button type="button" class="button-secondary" data-modal-close data-files-conflict-action="cancel">Cancel</button>
<button type="button" class="button-secondary" data-files-conflict-action="keep-both">Keep both</button>
<button type="button" data-files-conflict-action="overwrite">Overwrite</button>
</div>
</dialog>
<dialog id="files-delete-modal" class="modal" aria-labelledby="files-delete-title">
<div class="modal-header">
<h2 id="files-delete-title">Delete <span class="files-delete-name"></span>?</h2>
<button type="button" class="modal-close" data-modal-close aria-label="Close">&times;</button>
</div>
<div class="modal-body">
<p>This cannot be undone.</p>
<p class="files-delete-error muted" hidden></p>
</div>
<div class="modal-footer">
<button type="button" class="button-secondary" data-modal-close>Cancel</button>
<button type="button" class="danger files-delete-confirm">Delete</button>
</div>
</dialog>
<aside class="files-uploads" data-overlay-id="{{ overlay.id }}" hidden aria-live="polite">
<header class="files-uploads-header">
<strong class="files-uploads-title">Uploads</strong>
<button type="button" class="link-button files-uploads-clear" hidden>clear done</button>
</header>
<ul class="files-uploads-list"></ul>
</aside>
<script src="{{ url_for('static', filename='js/files-overlay.js') }}" defer></script>
{% endif %}
{% endblock %}