{# Plugin Configuration Partial - Server-side rendered form #} {# This template is loaded via HTMX when a plugin tab is clicked #} {% import 'v3/partials/_macros.html' as ui %} {# ===== MACROS FOR FORM FIELD GENERATION ===== #} {# Render a single form field based on schema type #} {# "x-display": "hidden" keeps a property declared -- so stored configs that still carry it keep validating -- without drawing a control for it (deprecated keys, internal ids). An object whose every child is hidden counts as hidden too, so it doesn't render as an empty section. Outputs "1" when hidden, nothing otherwise; test it with `|trim`. Nothing hidden is posted for a plain or nested field: the save deep-merges the form over the stored section, so the stored value stays. Array rows are the exception (a posted list replaces the stored one), see array-table. #} {% macro prop_is_hidden(prop) -%} {%- if prop is mapping -%} {%- if prop.get('x-display') == 'hidden' -%}1 {%- elif prop.get('properties') is mapping and prop.get('properties') -%} {%- set _hid = namespace(all=true) -%} {%- for _child in prop.get('properties').values() -%} {%- if not prop_is_hidden(_child)|trim -%}{%- set _hid.all = false -%}{%- endif -%} {%- endfor -%} {%- if _hid.all -%}1{%- endif -%} {%- endif -%} {%- endif -%} {%- endmacro %} {% macro render_field(key, prop, value, prefix='', plugin_id='') %} {% if not prop_is_hidden(prop)|trim %} {% set full_key = (prefix ~ '.' ~ key) if prefix else key %} {% set field_id = (plugin_id ~ '-' ~ full_key)|replace('.', '-')|replace('_', '-') %} {% set label = prop.title if prop.title else key|replace('_', ' ')|title %} {% set description = prop.description if prop.description else '' %} {% set _pt = prop.get('type') %} {% set field_type = _pt if (_pt is string) else ((_pt | first) if (_pt and _pt is iterable and _pt is not string) else 'string') %} {# Handle nested objects - check for widget first #} {% if field_type == 'object' %} {% set obj_widget = prop.get('x-widget') or prop.get('x_widget') %} {% if obj_widget == 'schedule-picker' %} {# Schedule picker widget - renders enable/mode/times UI #} {% set obj_value = value if value is not none else {} %}
Drag and drop images here or click to browse
Max {{ max_files }} files, {{ max_size_mb }}MB each (PNG, JPG, GIF, BMP)
Remember to save configuration after upload
{{ img.get('original_filename') or img.get('filename', 'Image') }}
{% if img.get('size') %}{{ (img.get('size') / 1024)|round }} KB{% endif %} {% if img.get('uploaded_at') %} • {{ img.get('uploaded_at') }}{% endif %}
{% if has_schedule %}Scheduled
{% endif %}Custom feeds widget requires 'name' and 'url' properties in items schema.
{% else %} {% set max_items = prop.get('maxItems', 50) %} {% set array_value = value if value is not none and value is iterable and value is not string else (prop.default if prop.default is defined and prop.default is iterable and prop.default is not string else []) %}| Name | URL | Logo | Enabled | Actions |
|---|---|---|---|---|
|
{% set logo_value = item.get('logo') or {} %}
{% set logo_path = logo_value.get('path', '') %}
{% if logo_path %}
|
| {{ col_title }} | {% endfor %}Actions |
|---|---|
|
{% if col_type == 'boolean' %}
{# Hidden sentinel ensures unchecked boxes still submit "false" (browsers
omit unchecked checkboxes entirely). It shares the checkbox's `name`,
so it must always mirror the checkbox's actual state — both here at
render time and on every toggle — or whichever of the two same-named
inputs the save request happens to prefer can silently revert this
field to false regardless of what's checked. #}
{% elif col_type == 'integer' or col_type == 'number' %}
{% elif col_enum %}
{# Labels are opt-in here and the fallback stays the raw
value: table columns hold things like ticker symbols,
which must not be title-cased behind the user's back. #}
{% set col_labels = (col_def.get('x-options') or col_def.get('x_options') or {}).get('labels') or {} %}
{% elif col_xwidget == 'date-picker' %}
{% elif col_xwidget == 'time-picker' %}
{% elif col_xwidget == 'file-upload-single' %}
{% set cell_input_id = field_id ~ '_' ~ item_index ~ '_' ~ col_name %}
{% if col_value %}
{% else %}
{% endif %}
|
{% endfor %}
{# Actions cell: delete + optional edit button for advanced props #}
{% set has_advanced = namespace(value=false) %}
{% for k, v in item_properties.items() %}{% if k not in display_columns and k != 'id' and not prop_is_hidden(v)|trim %}{% set has_advanced.value = true %}{% endif %}{% endfor %}
{# Hidden item properties (e.g. countdown's auto-generated id) get no control, but a posted row replaces the stored item wholesale, so carry the stored value through. JSON-encoded so the save can restore its exact type (an id "1" must stay a string); absent values are not invented. #} {% for k, v in item_properties.items() %}{% if prop_is_hidden(v)|trim and k in item and item[k] is not none %} {% endif %}{% endfor %} {% if has_advanced.value %} {% endif %} | {# Hidden cell: flat hidden inputs for non-displayed props (layout, style, etc.) #} {% if has_advanced.value %} {% set adv_schema = namespace(d={}) %} {% for k, v in item_properties.items() %}{% if k not in display_columns and k != 'id' and not prop_is_hidden(v)|trim %}{% set _ = adv_schema.d.update({k: v}) %}{% endif %}{% endfor %} {% endif %}
Separate multiple values with commas
{% endif %} {% endif %} {# String/default field - check for widgets #} {% else %} {% set str_widget = prop.get('x-widget') or prop.get('x_widget') %} {% set str_value = value if value is not none else (prop.default if prop.default is defined else '') %} {% if str_widget == 'file-upload' %} {# Single-file upload widget for string fields (e.g., credentials.json) #} {% set upload_config = prop.get('x-upload-config') or {} %} {% set upload_endpoint = upload_config.get('upload_endpoint', '') %} {% set target_filename = upload_config.get('target_filename', 'file.json') %} {% set max_size_mb = upload_config.get('max_size_mb', 1) %} {% set allowed_extensions = upload_config.get('allowed_extensions', ['.json']) %}Click to upload {{ target_filename }}
Max {{ max_size_mb }}MB ({{ allowed_extensions|join(', ') }})
Changes in the file manager save immediately — no need to click Save Configuration.
{% elif str_widget in ['text-input', 'textarea', 'select-dropdown', 'toggle-switch', 'radio-group', 'date-picker', 'time-picker', 'slider', 'color-picker', 'email-input', 'url-input', 'password-input', 'font-selector', 'file-upload-single', 'plugin-file-manager', 'google-oauth'] %} {# Render widget container #} {% else %} {# Default text input #} {% 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. #} {% endif %} {% endif %} {% endif %}{{ plugin.description or 'Plugin configuration' }}