mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-14 02:56:36 +00:00
feat(web): serve plugin-declared widgets, and actually ask for them
LEDMatrixWidgets.loadPluginWidget has always fetched /static/plugin-widgets/<plugin>/<widget>.js, and docs/widget-guide.md has always documented that path, but nothing served it. soccer-scoreboard has shipped a 17KB widgets/custom-leagues.js since August that could never load. Both halves were missing, not just the route: - serve_plugin_widget serves the script from the plugin's widgets/ directory as text/javascript. The manifest is the allowlist -- only a widget the plugin declares is reachable -- so installing a plugin does not publish everything it ships. Path handling mirrors the sibling serve_plugin_web_ui: allowlist regexes, os.path.basename, resolve() + relative_to() containment, and the ledmatrix- prefix fallback. The declared script name is guarded too, since it comes from the plugin rather than the request. - The config form never requested one. Its x-widget dispatch is a hardcoded list of core widget names, so a plugin's own widget fell through to a plain text input. An unrecognised x-widget on a string field now asks ensureWidget() for it. The text input stays as the fallback and is removed only once the widget has actually rendered, so a missing or broken widget costs the user an editor rather than their configured value on the next save. - manifest_schema.json gains "widgets", so the declaration is validated rather than merely tolerated by additionalProperties. Verified in a browser against the real partial: a declared widget loads, registers and renders, and its field posts exactly one value; a field whose widget 404s keeps its text input and still posts its value. Not addressed: loadPluginWidgetsFromManifest still has no caller. The per-field ensureWidget path is lazier and is what the form now uses, so that bulk helper is dead weight -- worth removing, but left alone here rather than inventing a call site for it. Known limitation, documented: only string-typed fields take this path. object/array/boolean/number fields and enums are dispatched by the template's own branches, which still only know core widgets. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -872,6 +872,65 @@
|
||||
name="{{ full_key }}"
|
||||
value="{{ str_value }}"
|
||||
class="form-input w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 bg-white text-black placeholder:text-gray-500">
|
||||
{% if str_widget %}
|
||||
{# An x-widget the core does not ship may be supplied by the
|
||||
plugin itself (manifest "widgets", served from its widgets/
|
||||
directory). Ask the loader for it; the text input above is
|
||||
the fallback and stays put unless the widget really renders,
|
||||
so a missing or broken widget degrades to an editable field
|
||||
rather than dropping the value on save. #}
|
||||
<div id="{{ field_id }}_container" class="{{ str_widget }}-container"></div>
|
||||
<script>
|
||||
(function() {
|
||||
var WIDGET = {{ str_widget|tojson|safe }};
|
||||
var PLUGIN = {{ plugin_id|tojson|safe }};
|
||||
function initPluginWidget() {
|
||||
if (!window.LEDMatrixWidgets || !window.LEDMatrixWidgets.ensureWidget) {
|
||||
setTimeout(initPluginWidget, 50);
|
||||
return;
|
||||
}
|
||||
var container = document.getElementById('{{ field_id }}_container');
|
||||
var fallback = document.getElementById('{{ field_id }}');
|
||||
if (!container) { return; }
|
||||
window.LEDMatrixWidgets.ensureWidget(WIDGET, PLUGIN).then(function(available) {
|
||||
if (!available) { container.remove(); return; }
|
||||
var widget = window.LEDMatrixWidgets.get(WIDGET);
|
||||
if (!widget || typeof widget.render !== 'function') { container.remove(); return; }
|
||||
var value = {{ str_value|tojson|safe }};
|
||||
var config = {
|
||||
'type': '{{ field_type }}',
|
||||
'enum': {{ (prop.enum or [])|tojson|safe }},
|
||||
'x-options': {{ (prop.get('x-options') or prop.get('x_options') or {})|tojson|safe }},
|
||||
'x-widget-config': {{ (prop.get('x-widget-config') or prop.get('x_widget_config') or {})|tojson|safe }}
|
||||
};
|
||||
try {
|
||||
widget.render(container, config, value, {
|
||||
fieldId: '{{ field_id }}',
|
||||
name: '{{ full_key }}',
|
||||
pluginId: PLUGIN
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('[PluginWidget] render failed for ' + WIDGET, e);
|
||||
container.remove();
|
||||
return;
|
||||
}
|
||||
// Only now is the widget authoritative for this
|
||||
// field; drop the fallback so the form posts one
|
||||
// value rather than two under the same name.
|
||||
if (fallback) { fallback.remove(); }
|
||||
}).catch(function(e) {
|
||||
console.warn('[PluginWidget] could not load ' + WIDGET, e);
|
||||
container.remove();
|
||||
});
|
||||
}
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initPluginWidget);
|
||||
} else {
|
||||
setTimeout(initPluginWidget, 50);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user