style(overlays): redesign create-overlay modal

Reorders fields to Name → Type → System-wide. Drops the legacy fieldset
border and the now-stale "path is generated automatically" hint. Type
radios use the new .radio-row vocabulary with always-visible
descriptions; the admin-only system-wide checkbox becomes a .switch-row
toggle. Form field names are unchanged, so the overlay-creation handler
is untouched.

Plan deviation: live_server e2e fixture now also sets LEFT4ME_ROOT.
This is required because the new test creates an overlay end-to-end via
the UI, and create_overlay_directory() writes under $LEFT4ME_ROOT,
which defaults to /var/lib/left4me (unwritable on dev machines).
The two existing live_server consumers (test_editor, test_smoke) only
visit /blueprints/<id> routes that don't touch the filesystem, so this
change is safe for them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
mwiegand 2026-05-19 00:31:16 +02:00
parent 6cce8b7be7
commit 34b65fcbbe
No known key found for this signature in database
3 changed files with 91 additions and 9 deletions

View file

@ -34,17 +34,48 @@
</div>
<div class="modal-body">
<input type="hidden" name="csrf_token" value="{{ session.get('csrf_token', '') }}">
<fieldset class="overlay-type-radio">
<legend>Type</legend>
<label><input type="radio" name="type" value="workshop" checked> Workshop (downloads from Steam)</label>
<label><input type="radio" name="type" value="script"> Script (runs sandboxed bash)</label>
<label><input type="radio" name="type" value="files"> Files (upload / edit text files online)</label>
</fieldset>
<label>Name <input name="name" required></label>
<div class="field">
<label class="field-label" for="create-overlay-name">Name</label>
<input id="create-overlay-name" name="name" required>
</div>
<div class="field">
<span class="field-label">Type</span>
<div class="radio-list">
<label class="radio-row">
<input type="radio" name="type" value="workshop" checked>
<span class="radio-row-text">
<strong>Workshop</strong>
<span>Downloads from Steam</span>
</span>
</label>
<label class="radio-row">
<input type="radio" name="type" value="script">
<span class="radio-row-text">
<strong>Script</strong>
<span>Runs sandboxed bash</span>
</span>
</label>
<label class="radio-row">
<input type="radio" name="type" value="files">
<span class="radio-row-text">
<strong>Files</strong>
<span>Upload / edit text files online</span>
</span>
</label>
</div>
</div>
{% if g.user and g.user.admin %}
<label><input type="checkbox" name="system_wide" value="1"> System-wide (visible to all users)</label>
<label class="switch-row">
<input type="checkbox" name="system_wide" value="1">
<span class="switch-row-text">
<strong>System-wide</strong>
<span>Visible to all users</span>
</span>
</label>
{% endif %}
<p class="muted">The path is generated automatically.</p>
</div>
<div class="modal-footer">
<button type="button" class="button-secondary" data-inline-modal-close>Cancel</button>

View file

@ -77,6 +77,10 @@ def login(page, base_url: str, username: str = "alice", password: str = "secret"
@pytest.fixture(scope="function")
def live_server(tmp_path, monkeypatch):
# Some routes (e.g. POST /overlays via create_overlay_directory) write
# under $LEFT4ME_ROOT. Point it at tmp_path so the prod default
# /var/lib/left4me doesn't kick in on dev machines.
monkeypatch.setenv("LEFT4ME_ROOT", str(tmp_path))
app = _boot_app(tmp_path, monkeypatch)
with session_scope() as session:

View file

@ -0,0 +1,47 @@
"""E2E test for the redesigned create-overlay modal."""
from __future__ import annotations
import pytest
from playwright.sync_api import Page, expect
from .conftest import login
pytestmark = pytest.mark.e2e
def test_create_overlay_modal_field_order_and_submission(page: Page, live_server) -> None:
base_url = live_server["base_url"]
login(page, base_url)
page.goto(f"{base_url}/overlays")
page.click('button[data-inline-modal-open="create-overlay-modal"]')
modal = page.locator("#create-overlay-modal")
expect(modal).to_be_visible()
# Field order: Name input must appear before Type radios in DOM/visual order.
name_input = modal.locator('input[name="name"]')
type_workshop = modal.locator('input[name="type"][value="workshop"]')
expect(name_input).to_be_visible()
expect(type_workshop).to_be_visible()
name_box = name_input.bounding_box()
type_box = type_workshop.bounding_box()
assert name_box is not None and type_box is not None
assert name_box["y"] < type_box["y"], "Name should sit above Type"
# No legacy fieldset border around Type group.
expect(modal.locator("fieldset.overlay-type-radio")).to_have_count(0)
# No legacy "path is generated automatically" copy anywhere.
expect(modal).not_to_contain_text("path is generated automatically")
# Default workshop selection is checked.
expect(type_workshop).to_be_checked()
# Submit with a unique name and the script type.
name_input.fill("e2e-test-overlay")
modal.locator('input[name="type"][value="script"]').check()
modal.locator('button[type="submit"]:has-text("Create")').click()
# Handler redirects to /overlays/<id> on success.
page.wait_for_url("**/overlays/*", timeout=5000)
expect(page.locator("h1")).to_contain_text("e2e-test-overlay")