{# 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 %} {# A key the saved config doesn't have renders its schema default. The route merges defaults first, but that merge stops at an object that declares its own default (often {}), leaving its children missing here. Booleans matter most: an unchecked box saves as false. #} {% if value is none and prop.default is defined %}{% set value = prop.default %}{% endif %} {% 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 {} %}
{% elif obj_widget == 'time-range' %} {# Time range widget - renders start/end time inputs #} {% set obj_value = value if value is not none else {} %}
{% elif obj_widget == 'style-editor' %} {# Composite per-element style editor. It renders its own inputs with the same dotted names the generic renderer would produce (customization.score_text.font, ...text_color.0, ...), so the save/validate/merge pipeline is untouched -- no hidden JSON blob, no new server-side parsing. It falls back to the normal nested rendering if the widget cannot be loaded. #} {% set obj_value = value if value is not none else {} %}
{{ render_nested_section(key, prop, value, prefix, plugin_id) }}
{% elif prop.properties %} {{ render_nested_section(key, prop, value, prefix, plugin_id) }} {% endif %} {% else %} {# Point the label at the control that actually takes focus. Widgets render their own element (e.g. _input), so a plain for="" would label nothing. Mirrors the branch order below; a miss just falls back to the old for="". #} {% set _lw = prop.get('x-widget') or prop.get('x_widget') %} {% set label_target = field_id %} {% if field_type == 'boolean' %} {% if _lw == 'toggle-switch' %}{% set label_target = field_id ~ '_button' %}{% endif %} {% elif prop.enum %} {% elif field_type in ['number', 'integer'] %} {% if _lw in ['slider', 'number-input'] %}{% set label_target = field_id ~ '_input' %}{% endif %} {% elif field_type == 'array' %} {% if _lw == 'color-picker' %}{% set label_target = field_id ~ '_hex' %} {% elif _lw == 'file-upload' %}{% set label_target = field_id ~ '_file_input' %}{% endif %} {% elif _lw in ['text-input', 'textarea', 'select-dropdown', 'date-picker', 'time-picker', 'slider', 'email-input', 'url-input', 'password-input', 'font-selector'] %} {% set label_target = field_id ~ '_input' %} {% elif _lw == 'color-picker' %} {% set label_target = field_id ~ '_color' %} {% endif %}
{# Boolean - check for widget first #} {% if field_type == 'boolean' %} {% set bool_widget = prop.get('x-widget') or prop.get('x_widget') %} {% if bool_widget == 'toggle-switch' %} {# Render toggle-switch widget #}
{% else %} {# Default checkbox - value="true" ensures checked sends "true" not "on" #} {% endif %} {# Enum dropdown. Option text comes from x-options.labels when the schema supplies it -- the same convention the checkbox-group widget already uses -- because humanising the raw value cannot express every label: "vs" reads as "Vs", and "abbrev" says nothing about the "Sep 19" it produces. Values without a label fall back to the humanised form, so existing schemas render exactly as before. #} {% elif prop.enum %} {% set enum_labels = (prop.get('x-options') or prop.get('x_options') or {}).get('labels') or {} %} {# Number input - check for widget first #} {% elif field_type in ['number', 'integer'] %} {% set num_widget = prop.get('x-widget') or prop.get('x_widget') %} {% if num_widget in ['slider', 'number-input'] %} {# Render slider or number-input widget #}
{% else %} {# Default number input #} {% endif %} {# Array - check for file upload widget first (to avoid breaking static-image plugin), then checkbox-group, then array of objects #} {% elif field_type == 'array' %} {# An array of objects with no widget named would otherwise reach the comma-separated text input, which joins each item as a Python dict repr and posts it back as a list of strings -- the schema rejects that and every save of the plugin returns 400. The table widget already edits any array of objects, so make it the default. #} {% set _items = prop.get('items') or {} %} {% set x_widget = prop.get('x-widget') or prop.get('x_widget') or ('array-table' if (_items.get('type') == 'object' and _items.get('properties')) else none) %} {% if x_widget == 'file-upload' %} {# File upload widget for arrays #} {% set upload_config = prop.get('x-upload-config') or {} %} {% set max_files = upload_config.get('max_files', 10) %} {% set allowed_types = upload_config.get('allowed_types', ['image/png', 'image/jpeg', 'image/bmp', 'image/gif']) %} {% set max_size_mb = upload_config.get('max_size_mb', 5) %} {% set plugin_id_from_config = upload_config.get('plugin_id', plugin_id) %} {% 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 []) %}

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

{% for img in array_value %} {% set img_id = img.get('id', loop.index0) %} {% set img_schedule = img.get('schedule', {}) %} {% set has_schedule = img_schedule.get('enabled', false) and img_schedule.get('mode') and img_schedule.get('mode') != 'always' %}
{{ img.get('filename', '') }}

{{ 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 %}

{# file-upload.js rewrites .image-schedule-summary in place when the schedule is edited #}

{{ 'Scheduled' if has_schedule else 'Always shown' }}

{% endfor %}
{% elif x_widget == 'checkbox-group' %} {# Checkbox group widget for multi-select arrays with enum items #} {% 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 []) %} {% set items_schema = prop.get('items') or {} %} {% set enum_items = items_schema.get('enum') or [] %} {% set x_options = prop.get('x-options') or {} %} {% set labels = x_options.get('labels') or {} %} {# A saved value that is no longer one of the options -- a team code the league retired, an option dropped from the schema -- has no checkbox to render, so it would sit unseen in the hidden input below and be posted back on save. The schema rejects it and the save endpoint returns 400, which blocks editing any other field on the plugin until the stale entry is found and removed. Drop them here instead, and say which, so the value is not lost silently. Only when the widget actually has options: an empty enum means nothing to check against, and filtering on it would wipe the field. #} {% set stale_values = (array_value | reject('in', enum_items) | list) if enum_items else [] %} {% set array_value = (array_value | select('in', enum_items) | list) if enum_items else array_value %} {% if stale_values %}
No longer offered, and will be removed when you save: {{ stale_values | join(', ') }}.
{% endif %}
{% for option in enum_items %} {% set is_checked = option in array_value %} {% set option_label = labels.get(option, option|replace('_', ' ')|title) %} {% set checkbox_id = (field_id ~ '_' ~ option)|replace('.', '_')|replace(' ', '_') %} {% endfor %}
{# Hidden input to store selected values as JSON array (like array-of-objects pattern) #} {# Sentinel hidden input with bracket notation to allow clearing array to [] when all unchecked #} {# This ensures the field is always submitted, even when all checkboxes are unchecked #} {% elif x_widget == 'google-calendar-picker' %} {# Google Calendar picker — dynamically loads calendars from the API #} {# Normalise: if value is a string (legacy comma-separated), split it; otherwise fall back to default or [] #} {% if value is not none and value is string and value %} {% set array_value = value.split(',') | map('trim') | list %} {% elif value is not none and value is iterable and value is not string %} {% set array_value = value %} {% elif prop.default is defined and prop.default is string and prop.default %} {% set array_value = prop.default.split(',') | map('trim') | list %} {% elif prop.default is defined and prop.default is iterable and prop.default is not string %} {% set array_value = prop.default %} {% else %} {% set array_value = [] %} {% endif %}
{% elif x_widget == 'day-selector' %} {# Day selector widget for selecting days of the week #} {% 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 []) %}
{% else %} {# Check for custom-feeds widget first #} {% set items_schema = prop.get('items') or {} %} {% if x_widget == 'custom-feeds' %} {# Custom feeds table interface - widget-specific implementation #} {# Validate that required fields exist in schema #} {% set item_properties = items_schema.get('properties', {}) %} {% if not (item_properties.get('name') and item_properties.get('url')) %} {# Fallback to generic if schema doesn't match expected structure #}

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 []) %}
{% for item in array_value %} {% set item_index = loop.index0 %} {% endfor %}
Name URL Logo Enabled Actions
{% set logo_value = item.get('logo') or {} %} {% set logo_path = logo_value.get('path', '') %}
{% if logo_path %} Logo {# Only when there is one, as custom-feeds.js does: a blank id is saved as null and fails the id's string type. #} {% if logo_value.get('id') %} {% endif %} {% else %} No logo {% endif %}
{% endif %} {% elif x_widget == 'array-table' %} {# Generic array-of-objects table widget - reads columns from schema #} {% set item_properties = items_schema.get('properties', {}) %} {% 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 []) %} {# Use x-columns if specified, otherwise auto-detect first 4 simple properties #} {# Hidden item properties never become columns (see prop_is_hidden). #} {% set x_columns = prop.get('x-columns') %} {% if x_columns %} {% set display_columns = [] %} {% for col_name in x_columns %} {% if not prop_is_hidden(item_properties.get(col_name))|trim %} {% set _ = display_columns.append(col_name) %} {% endif %} {% endfor %} {% else %} {% set display_columns = [] %} {% for col_name in item_properties.keys() %} {% set col_def = item_properties[col_name] %} {% if col_def.get('type') not in ['object', 'array'] and display_columns|length < 4 and not prop_is_hidden(col_def)|trim %} {% set _ = display_columns.append(col_name) %} {% endif %} {% endfor %} {% endif %}
{% for col_name in display_columns %} {% set col_def = item_properties.get(col_name, {}) %} {% set col_title = col_def.get('title', col_name|replace('_', ' ')|title) %} {% set col_xwidget = col_def.get('x-widget') or col_def.get('x_widget', '') %} {% set col_enum = col_def.get('enum', []) %} {% set _raw_ctype = col_def.get('type', 'string') %} {% if _raw_ctype is iterable and _raw_ctype is not string %} {% set col_ctype = (_raw_ctype | reject('equalto','null') | list | first) or 'string' %} {% else %} {% set col_ctype = _raw_ctype or 'string' %} {% endif %} {% if col_xwidget == 'date-picker' %}{% set col_min_w = '140px' %} {% elif col_xwidget == 'time-picker' %}{% set col_min_w = '115px' %} {% elif col_xwidget == 'file-upload-single' %}{% set col_min_w = '200px' %} {% elif col_enum %}{% set col_min_w = '90px' %} {% elif col_ctype == 'boolean' %}{% set col_min_w = '60px' %} {% elif col_ctype in ['integer', 'number'] %}{% set col_min_w = '80px' %} {% else %}{% set col_min_w = '110px' %}{% endif %} {% endfor %} {% for item in array_value %} {% set item_index = loop.index0 %} {% for col_name in display_columns %} {% set col_def = item_properties.get(col_name, {}) %} {# Accessible name for the cell's control: column title + row. #} {% set cell_label = col_def.get('title', col_name|replace('_', ' ')|title) ~ ', row ' ~ (item_index + 1) %} {# Normalize nullable types e.g. ["null","integer"] → "integer" #} {% set _raw_type = col_def.get('type', 'string') %} {% if _raw_type is iterable and _raw_type is not string %} {% set col_type = (_raw_type | reject('equalto','null') | list | first) or 'string' %} {% else %} {% set col_type = _raw_type or 'string' %} {% endif %} {% set col_xwidget = col_def.get('x-widget') or col_def.get('x_widget', '') %} {% set col_enum = col_def.get('enum', []) %} {% set col_value = item.get(col_name, col_def.get('default', '')) %} {% if col_xwidget == 'date-picker' %}{% set td_min_w = '140px' %} {% elif col_xwidget == 'time-picker' %}{% set td_min_w = '115px' %} {% elif col_xwidget == 'file-upload-single' %}{% set td_min_w = '200px' %} {% elif col_enum %}{% set td_min_w = '90px' %} {% elif col_type == 'boolean' %}{% set td_min_w = '60px' %} {% elif col_type in ['integer', 'number'] %}{% set td_min_w = '80px' %} {% else %}{% set td_min_w = '110px' %}{% 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 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 %} {% endfor %}
{{ col_title }}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 %}{% endif %}
{% else %} {% endif %}
{# 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 %}
{# end overflow-x:auto wrapper #}
{% elif x_widget == 'color-picker' %} {# RGB color array: R / G / B number inputs + visual swatch + sync'd hex picker #} {% set color_arr = 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 [255, 255, 255]) %} {% set r_val = color_arr[0] if color_arr|length > 0 else 255 %} {% set g_val = color_arr[1] if color_arr|length > 1 else 255 %} {% set b_val = color_arr[2] if color_arr|length > 2 else 255 %} {% set hex_val = '#%02x%02x%02x' % (r_val|int, g_val|int, b_val|int) %}
{% else %} {# Generic array-of-objects would go here if needed in the future #} {# For now, fall back to regular array input (comma-separated) #} {# Regular array input (comma-separated) #} {% set array_value = value if value is not none else (prop.default if prop.default is defined else []) %}

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(', ') }})

{% elif str_widget == 'json-file-manager' %} {# Embedded file manager — plugin's web_ui/file_manager.html served via /v3/plugin-ui/ route #}

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 %}
{% endif %} {% endif %}{# not prop_is_hidden #} {% endmacro %} {# Render a nested/collapsible section for object types #} {% macro render_nested_section(key, prop, value, prefix='', plugin_id='') %} {% set full_key = (prefix ~ '.' ~ key) if prefix else key %} {% set section_id = (plugin_id ~ '-section-' ~ full_key)|replace('.', '-')|replace('_', '-') %} {% set label = prop.title if prop.title else key|replace('_', ' ')|title %} {% set description = prop.description if prop.description else '' %} {# Only a dict can be looked into. A schema that turns a legacy scalar into an object (news' dynamic_duration: true -> {enabled, ...}) still finds the scalar in config.json until the next save, and `key in true` is a TypeError that takes down the whole config page. A legacy boolean was the on/off switch the object's `enabled` now holds, so carry it over -- rendering {} would draw that checkbox unchecked and the next save would quietly turn the feature off. The save then writes the object. The plugin loader applies the same rule before validating (legacy_bool_as_object in src/plugin_system/schema_manager.py), and test/test_legacy_boolean_config.py renders this macro against it: change the two together. #} {% if value is mapping %} {% set nested_value = value %} {% elif value is boolean and 'enabled' in (prop.properties or {}) %} {% set nested_value = {'enabled': value} %} {% else %} {% set nested_value = {} %} {% endif %}
{% endmacro %} {# ===== MAIN TEMPLATE ===== #}

{{ plugin.name or plugin.id }}

{{ plugin.description or 'Plugin configuration' }}

{# Plugin Information Panel #}

Plugin Information

Name
{{ plugin.name or plugin.id }}
Author
{{ plugin.author or 'Unknown' }}
{% if plugin.version %}
Version
{{ plugin.version }}
{% endif %} {% if plugin.last_commit %}
Commit
{{ plugin.last_commit[:7] if plugin.last_commit|length > 7 else plugin.last_commit }} {% if plugin.branch %} ({{ plugin.branch }}) {% endif %}
{% endif %} {% if plugin.category %}
Category
{{ plugin.category }}
{% endif %} {% if plugin.tags %}
Tags
{% for tag in plugin.tags %} {{ tag }} {% endfor %}
{% endif %}
{# On-Demand Controls #}
On-Demand Controls
{% if not plugin.enabled %}

Plugin is disabled, but on-demand will temporarily enable it.

{% endif %}
{# Configuration Form Panel #}

Configuration

{{ ui.settings_filter("Filter this plugin's settings…") }}
{% if schema and schema.properties %} {# Use property order if defined, otherwise use natural order #} {# Skip 'enabled' field - it's handled by the header toggle #} {% set property_order = schema['x-propertyOrder'] if 'x-propertyOrder' in schema else schema.properties.keys()|list %} {# Flat (non-object) properties flagged "x-advanced": true are grouped into one collapsed "Advanced Settings" section after the basic fields. Object-type properties already render as their own collapsible sections, so the flag is ignored for them. Schemas without the flag render exactly as before. #} {% set tiers = namespace(basic=[], advanced=[]) %} {% for key in property_order %} {# A hidden key is neither drawn nor reported in __rendered_section, so the save never reads its absence as an unchecked box. #} {% if key in schema.properties and key != 'enabled' and not prop_is_hidden(schema.properties[key])|trim %} {% set prop = schema.properties[key] %} {% set is_object = prop.type is defined and 'object' in prop.type %} {% if prop.get('x-advanced') and not is_object %} {% set tiers.advanced = tiers.advanced + [key] %} {% else %} {% set tiers.basic = tiers.basic + [key] %} {% endif %} {% endif %} {% endfor %} {# Tell the save path which sections this form actually drew. An unchecked checkbox posts nothing, so without this the server cannot tell "the user cleared it" from "the caller never had that field" -- and a partial post would read as every box being off. #} {% for key in tiers.basic + tiers.advanced %} {% endfor %} {% for key in tiers.basic %} {% set prop = schema.properties[key] %} {% set value = config[key] if key in config else none %} {{ render_field(key, prop, value, '', plugin.id) }} {% endfor %} {% if tiers.advanced %} {% set adv_section_id = (plugin.id ~ '-section-advanced-settings')|replace('.', '-')|replace('_', '-') %}
{% endif %} {% else %} {# No schema - render simple form from config #} {% if config %} {% for key, value in config.items() %} {% if key not in ['enabled'] %}
{% set fallback_id = 'fallback-field-' ~ (key|string|replace(' ', '-')) %} {% if value is sameas true or value is sameas false %} {% elif value is number %} {% else %} {% endif %}
{% endif %} {% endfor %} {% else %}

No configuration options available for this plugin.

{% endif %} {% endif %}
{# Web UI Actions — hide if schema has a dedicated file-manager widget, or if every action is marked ui_hidden in the manifest. #} {% set has_file_manager_widget = namespace(value=false) %} {% for _fk, _fp in schema.get('properties', {}).items() %} {% if (_fp.get('x-widget') or _fp.get('x_widget')) in ('json-file-manager', 'plugin-file-manager') %} {% set has_file_manager_widget.value = true %} {% endif %} {% endfor %} {% set visible_actions = [] %} {% for _a in web_ui_actions %} {% if not _a.get('ui_hidden', false) %} {% set _ = visible_actions.append(_a) %} {% endif %} {% endfor %} {% if visible_actions and not has_file_manager_widget.value %}

Plugin Actions

{% if visible_actions[0].section_description %}

{{ visible_actions[0].section_description }}

{% endif %}
{% for action in visible_actions %} {% set action_id = "action-" ~ action.id ~ "-" ~ loop.index0 %} {% set status_id = "action-status-" ~ action.id ~ "-" ~ loop.index0 %} {% set bg_color = action.color or 'blue' %} {% if bg_color == 'green' %} {% set bg_class = 'bg-green-50' %} {% set border_class = 'border-green-200' %} {% set text_class = 'text-green-900' %} {% set text_light_class = 'text-green-700' %} {% set btn_class = 'bg-green-600 hover:bg-green-700' %} {% elif bg_color == 'red' %} {% set bg_class = 'bg-red-50' %} {% set border_class = 'border-red-200' %} {% set text_class = 'text-red-900' %} {% set text_light_class = 'text-red-700' %} {% set btn_class = 'bg-red-600 hover:bg-red-700' %} {% elif bg_color == 'yellow' %} {% set bg_class = 'bg-yellow-50' %} {% set border_class = 'border-yellow-200' %} {% set text_class = 'text-yellow-900' %} {% set text_light_class = 'text-yellow-700' %} {% set btn_class = 'bg-yellow-600 hover:bg-yellow-700' %} {% elif bg_color == 'purple' %} {% set bg_class = 'bg-purple-50' %} {% set border_class = 'border-purple-200' %} {% set text_class = 'text-purple-900' %} {% set text_light_class = 'text-purple-700' %} {% set btn_class = 'bg-purple-600 hover:bg-purple-700' %} {% else %} {% set bg_class = 'bg-blue-50' %} {% set border_class = 'border-blue-200' %} {% set text_class = 'text-blue-900' %} {% set text_light_class = 'text-blue-700' %} {% set btn_class = 'bg-blue-600 hover:bg-blue-700' %} {% endif %}

{% if action.icon %}{% endif %}{{ action.title or action.id }}

{{ action.description or '' }}

{% endfor %}
{% endif %} {# Action Buttons #}