feat(blueprint): mount srccfg editor on the config textarea

The textarea is preserved as the form field; the editor renders a
contenteditable sibling and mirrors content back on every input. Form
POST contract is untouched (covered by new round-trip test).
This commit is contained in:
mwiegand 2026-05-16 19:39:58 +02:00
parent b203a83f58
commit 607970eb43
No known key found for this signature in database
2 changed files with 53 additions and 1 deletions

View file

@ -49,7 +49,7 @@
<pre class="config-preview" aria-label="Auto-loaded overlay configs">{% for o in exposed %}exec {{ o.name }}.cfg
{% endfor %}</pre>
{% endif %}
<textarea name="config" rows="8" spellcheck="false">{{ config_lines | join('\n') }}</textarea>
<textarea name="config" rows="8" spellcheck="false" data-editor-language="srccfg">{{ config_lines | join('\n') }}</textarea>
</div>
</label>
<button type="submit">Save blueprint</button>
@ -92,4 +92,5 @@
</div>
</dialog>
<script src="{{ url_for('static', filename='js/blueprint-overlay-picker.js') }}" defer></script>
{% include '_editor_assets.html' %}
{% endblock %}

View file

@ -253,6 +253,57 @@ def test_form_update_preserves_ordered_overlays_and_multiline_fields(user_client
assert update.headers["Location"] == "/blueprints/1"
def test_blueprint_detail_renders_editor_assets(user_client) -> None:
with session_scope() as session:
user = session.scalar(select(User).where(User.username == "alice"))
blueprint = Blueprint(user_id=user.id, name="bp", arguments="[]", config="[]")
session.add(blueprint)
session.flush()
blueprint_id = blueprint.id
response = user_client.get(f"/blueprints/{blueprint_id}")
assert response.status_code == 200
body = response.get_data(as_text=True)
# Editor opts the textarea in via a data-attribute.
assert 'data-editor-language="srccfg"' in body
# All editor assets are referenced.
assert "static/vendor/prism.js" in body
assert "static/vendor/codejar.js" in body
assert "static/js/srccfg-grammar.js" in body
assert "static/js/editor.js" in body
assert "static/css/editor.css" in body
# Scripts are nonce'd (CSP regression guard).
assert 'nonce="' in body
def test_blueprint_config_form_post_still_round_trips(user_client) -> None:
# The editor is a visual layer; the form POST contract must be
# unaffected. This guards against accidentally renaming `config` or
# dropping it from form serialization.
with session_scope() as session:
user = session.scalar(select(User).where(User.username == "alice"))
blueprint = Blueprint(user_id=user.id, name="bp", arguments="[]", config="[]")
session.add(blueprint)
session.flush()
blueprint_id = blueprint.id
update = user_client.post(
f"/blueprints/{blueprint_id}",
data={
"name": "bp",
"arguments": "",
"config": "sv_cheats 1\nmp_gamemode coop",
"overlay_ids": [],
},
headers={"X-CSRF-Token": "test-token"},
)
assert update.status_code == 302
with session_scope() as session:
bp = session.get(Blueprint, blueprint_id)
assert json.loads(bp.config) == ["sv_cheats 1", "mp_gamemode coop"]
def test_blueprint_detail_renders_picker_list_and_select(user_client) -> None:
with session_scope() as session:
session.add(Overlay(name="o3", path="/opt/l4d2/overlays/o3"))