mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-08-08 12:18:06 +00:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8d700e5d9 | ||
|
|
e0d62a9e54 | ||
|
|
fc25a70d75 | ||
|
|
003312f4ff |
@@ -149,7 +149,7 @@
|
|||||||
"min_plugin_width": 8,
|
"min_plugin_width": 8,
|
||||||
"lead_in_width": 0,
|
"lead_in_width": 0,
|
||||||
"plugins_per_cycle": 6,
|
"plugins_per_cycle": 6,
|
||||||
"max_plugin_width_ratio": 3.0,
|
"max_plugin_width_ratio": 0.0,
|
||||||
"overflow_mode": "rotate",
|
"overflow_mode": "rotate",
|
||||||
"dynamic_duration_enabled": true,
|
"dynamic_duration_enabled": true,
|
||||||
"min_cycle_duration": 60,
|
"min_cycle_duration": 60,
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ Read by `src/vegas_mode/config.py` (`VegasScrollConfig.from_config`). See
|
|||||||
| `min_plugin_width` | int, `8` |
|
| `min_plugin_width` | int, `8` |
|
||||||
| `lead_in_width` | int, `0` |
|
| `lead_in_width` | int, `0` |
|
||||||
| `plugins_per_cycle` | int, `6` |
|
| `plugins_per_cycle` | int, `6` |
|
||||||
| `max_plugin_width_ratio` | float, `3.0` |
|
| `max_plugin_width_ratio` | float, `0.0` |
|
||||||
| `overflow_mode` | string, `"rotate"` |
|
| `overflow_mode` | string, `"rotate"` |
|
||||||
| `dynamic_duration_enabled` | bool, `true` |
|
| `dynamic_duration_enabled` | bool, `true` |
|
||||||
| `min_cycle_duration` | int, `60` |
|
| `min_cycle_duration` | int, `60` |
|
||||||
|
|||||||
@@ -206,6 +206,47 @@ To use an existing widget in your plugin's `config_schema.json`, simply add the
|
|||||||
|
|
||||||
The widget will be automatically rendered when the plugin configuration form is loaded.
|
The widget will be automatically rendered when the plugin configuration form is loaded.
|
||||||
|
|
||||||
|
## Labelling Enum Options (`x-options.labels`)
|
||||||
|
|
||||||
|
A plain `enum` renders as a dropdown whose option text is the value with
|
||||||
|
underscores replaced and title case applied — `day_first` becomes "Day First".
|
||||||
|
That is fine for values that read as their own label, and wrong for values that
|
||||||
|
do not: `vs` becomes "Vs", and `abbrev` says nothing about the `Sep 19` it
|
||||||
|
actually produces.
|
||||||
|
|
||||||
|
Supply `x-options.labels` to set the visible text. This is the same convention
|
||||||
|
the `checkbox-group` widget uses:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"date_format": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["abbrev", "numeric", "day_first"],
|
||||||
|
"default": "abbrev",
|
||||||
|
"x-options": {
|
||||||
|
"labels": {
|
||||||
|
"abbrev": "Sep 19",
|
||||||
|
"numeric": "9/19",
|
||||||
|
"day_first": "19 Sep"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Labels are **display only** — the stored value is still the enum value, so
|
||||||
|
adding them never changes a saved config. The map may be partial: any value
|
||||||
|
without a label keeps the humanised fallback. Older cores that predate this
|
||||||
|
support ignore `x-options` and render the fallback for every option, so a
|
||||||
|
plugin can ship labels without requiring a core upgrade.
|
||||||
|
|
||||||
|
Array-table columns (`x-widget: array-table`) accept the same
|
||||||
|
`x-options.labels` on a column definition, but their fallback is the **raw
|
||||||
|
value** rather than the humanised one, because those columns hold values such
|
||||||
|
as ticker symbols where `aapl` → "Aapl" would be wrong. Rows added in the
|
||||||
|
browser use the labels too (`array-table.js`), so a column reads the same
|
||||||
|
before and after a page reload.
|
||||||
|
|
||||||
## Marking Fields as Advanced (`x-advanced`)
|
## Marking Fields as Advanced (`x-advanced`)
|
||||||
|
|
||||||
Add `"x-advanced": true` to any top-level, non-object property to move it out
|
Add `"x-advanced": true` to any top-level, non-object property to move it out
|
||||||
|
|||||||
@@ -104,10 +104,22 @@ class VegasModeConfig:
|
|||||||
overflow_mode: str = "rotate"
|
overflow_mode: str = "rotate"
|
||||||
|
|
||||||
# Cap on one plugin's share of a cycle, as a multiple of display width.
|
# Cap on one plugin's share of a cycle, as a multiple of display width.
|
||||||
# A single ticker returning 7,000px would otherwise hold the panel for over
|
# 0 (the default) disables the cap, so every plugin contributes all of its
|
||||||
# two minutes. Overflow is deferred to later cycles rather than discarded.
|
# content and is always entered at its beginning.
|
||||||
# 0 disables the cap.
|
#
|
||||||
max_plugin_width_ratio: float = 3.0
|
# Capping was the default until it proved to cost more than it bought.
|
||||||
|
# Measured over a 17-plugin fleet on a 512px panel, only four plugins were
|
||||||
|
# ever wide enough to hit a 3.0 cap; for those four it produced two visible
|
||||||
|
# faults. Content resumed mid-item on each appearance (a news ticker entered
|
||||||
|
# at column 6027 of its own strip), and the final window of a rotation was
|
||||||
|
# whatever happened to be left — 348px of a 1840px stocks ticker, seven
|
||||||
|
# seconds of panel time. Both read as the display being broken rather than
|
||||||
|
# as deferral working.
|
||||||
|
#
|
||||||
|
# A wide plugin does hold the panel for a long time uncapped: set the cap
|
||||||
|
# per plugin with vegas_max_width_screens where that matters, rather than
|
||||||
|
# globally where it mostly hurts plugins that were never the problem.
|
||||||
|
max_plugin_width_ratio: float = 0.0
|
||||||
|
|
||||||
# Plugin management
|
# Plugin management
|
||||||
plugin_order: List[str] = field(default_factory=list)
|
plugin_order: List[str] = field(default_factory=list)
|
||||||
@@ -159,7 +171,7 @@ class VegasModeConfig:
|
|||||||
lead_in_width=int(vegas_config.get('lead_in_width', 0)),
|
lead_in_width=int(vegas_config.get('lead_in_width', 0)),
|
||||||
plugins_per_cycle=int(vegas_config.get('plugins_per_cycle', 6)),
|
plugins_per_cycle=int(vegas_config.get('plugins_per_cycle', 6)),
|
||||||
max_plugin_width_ratio=float(
|
max_plugin_width_ratio=float(
|
||||||
vegas_config.get('max_plugin_width_ratio', 3.0)),
|
vegas_config.get('max_plugin_width_ratio', 0.0)),
|
||||||
overflow_mode=str(vegas_config.get('overflow_mode', 'rotate')),
|
overflow_mode=str(vegas_config.get('overflow_mode', 'rotate')),
|
||||||
plugin_order=list(vegas_config.get('plugin_order', [])),
|
plugin_order=list(vegas_config.get('plugin_order', [])),
|
||||||
excluded_plugins=set(vegas_config.get('excluded_plugins', [])),
|
excluded_plugins=set(vegas_config.get('excluded_plugins', [])),
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
"""Guard: enum dropdowns in the plugin config form honour x-options.labels.
|
||||||
|
|
||||||
|
The form derives an option's visible text from its value — underscores
|
||||||
|
replaced, title case applied ("day_first" -> "Day First"). That cannot
|
||||||
|
express every label a schema needs: "vs" reads as "Vs", and "abbrev" says
|
||||||
|
nothing about the "Sep 19" it produces. Schemas can supply x-options.labels
|
||||||
|
instead, the same convention the checkbox-group widget already uses.
|
||||||
|
|
||||||
|
These tests extract the enum <select> block *out of the shipped template*
|
||||||
|
and render that, so they exercise the production expression rather than a
|
||||||
|
copy of it. If the fallback or the lookup changes, these tests render the
|
||||||
|
changed code and fail — a duplicated fragment here would silently keep
|
||||||
|
passing.
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from jinja2 import DictLoader, Environment
|
||||||
|
|
||||||
|
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
CONFIG_FORM = (PROJECT_ROOT / 'web_interface' / 'templates' / 'v3' / 'partials'
|
||||||
|
/ 'plugin_config.html')
|
||||||
|
ARRAY_TABLE_JS = (PROJECT_ROOT / 'web_interface' / 'static' / 'v3' / 'js'
|
||||||
|
/ 'widgets' / 'array-table.js')
|
||||||
|
|
||||||
|
# The enum branch: from the `{% set enum_labels %}` line through `</select>`.
|
||||||
|
ENUM_BLOCK_RE = re.compile(
|
||||||
|
r"(\{%\s*set enum_labels\s*=.*?</select>)", re.S
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _shipped_enum_block() -> str:
|
||||||
|
"""Return the live enum <select> block lifted from plugin_config.html."""
|
||||||
|
source = CONFIG_FORM.read_text(encoding='utf-8')
|
||||||
|
match = ENUM_BLOCK_RE.search(source)
|
||||||
|
assert match, (
|
||||||
|
'could not find the enum <select> block in plugin_config.html — the '
|
||||||
|
'template changed shape and this guard needs updating'
|
||||||
|
)
|
||||||
|
return match.group(1)
|
||||||
|
|
||||||
|
|
||||||
|
def _render(prop: dict, value=None) -> str:
|
||||||
|
"""Render the shipped enum block with a minimal fixture."""
|
||||||
|
env = Environment(loader=DictLoader({'f': _shipped_enum_block()}),
|
||||||
|
autoescape=True)
|
||||||
|
return env.get_template('f').render(
|
||||||
|
prop=prop, value=value, field_id='fid', full_key='k'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _option_labels(html: str) -> dict:
|
||||||
|
"""Map each rendered option's value to its visible text."""
|
||||||
|
return {
|
||||||
|
value: text.strip()
|
||||||
|
for value, text in re.findall(
|
||||||
|
r'<option value="([^"]*)"[^>]*>(.*?)</option>', html, re.S
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_labels_are_used_when_supplied() -> None:
|
||||||
|
html = _render({
|
||||||
|
'enum': ['vs', 'date_time'],
|
||||||
|
'x-options': {'labels': {'vs': 'VS', 'date_time': 'Date and time'}},
|
||||||
|
})
|
||||||
|
assert _option_labels(html) == {'vs': 'VS', 'date_time': 'Date and time'}
|
||||||
|
|
||||||
|
|
||||||
|
def test_unlabelled_values_keep_the_humanised_fallback() -> None:
|
||||||
|
"""Schemas without labels must render exactly as they did before."""
|
||||||
|
html = _render({'enum': ['day_first', 'weekday']})
|
||||||
|
assert _option_labels(html) == {'day_first': 'Day First', 'weekday': 'Weekday'}
|
||||||
|
|
||||||
|
|
||||||
|
def test_partial_labels_fall_back_per_value() -> None:
|
||||||
|
"""A labels map covering some values leaves the rest humanised."""
|
||||||
|
html = _render({'enum': ['vs', 'day_first'],
|
||||||
|
'x-options': {'labels': {'vs': 'VS'}}})
|
||||||
|
assert _option_labels(html) == {'vs': 'VS', 'day_first': 'Day First'}
|
||||||
|
|
||||||
|
|
||||||
|
def test_option_values_are_unchanged_by_labelling() -> None:
|
||||||
|
"""Labels are display-only: the submitted value stays the enum value."""
|
||||||
|
html = _render({'enum': ['abbrev'],
|
||||||
|
'x-options': {'labels': {'abbrev': 'Sep 19'}}})
|
||||||
|
assert _option_labels(html) == {'abbrev': 'Sep 19'}
|
||||||
|
|
||||||
|
|
||||||
|
def test_selected_option_still_tracks_the_current_value() -> None:
|
||||||
|
"""Labelling must not disturb which option is marked selected."""
|
||||||
|
html = _render({'enum': ['abbrev', 'numeric'],
|
||||||
|
'x-options': {'labels': {'abbrev': 'Sep 19'}}},
|
||||||
|
value='numeric')
|
||||||
|
selected = re.search(r'<option value="([^"]+)"[^>]*selected', html)
|
||||||
|
assert selected and selected.group(1) == 'numeric'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('key', ['x-options', 'x_options'])
|
||||||
|
def test_both_option_key_spellings_work(key: str) -> None:
|
||||||
|
"""The template accepts either spelling, as its other widgets do."""
|
||||||
|
html = _render({'enum': ['vs'], key: {'labels': {'vs': 'VS'}}})
|
||||||
|
assert _option_labels(html) == {'vs': 'VS'}
|
||||||
|
|
||||||
|
|
||||||
|
def test_table_column_enum_falls_back_to_the_raw_value() -> None:
|
||||||
|
"""Array-table columns must not title-case values that were never labelled.
|
||||||
|
|
||||||
|
Those columns hold values such as ticker symbols, where "aapl" -> "Aapl"
|
||||||
|
would be wrong, so their fallback stays the raw value.
|
||||||
|
"""
|
||||||
|
source = CONFIG_FORM.read_text(encoding='utf-8')
|
||||||
|
assert 'col_labels.get(opt, opt)' in source, (
|
||||||
|
'array-table column options must fall back to the raw value, not the '
|
||||||
|
'humanised one'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_dynamically_added_table_rows_use_the_same_labels() -> None:
|
||||||
|
"""Rows added client-side must label options like the server-rendered ones.
|
||||||
|
|
||||||
|
array-table.js builds new rows in the browser; if it printed the raw value
|
||||||
|
a column would read differently before and after a page reload.
|
||||||
|
"""
|
||||||
|
js = ARRAY_TABLE_JS.read_text(encoding='utf-8')
|
||||||
|
assert 'function enumOptionLabel' in js, (
|
||||||
|
'array-table.js lost its enum label helper'
|
||||||
|
)
|
||||||
|
raw_option_text = re.findall(r'o\.textContent\s*=\s*opt\s*;', js)
|
||||||
|
assert not raw_option_text, (
|
||||||
|
'array-table.js renders an enum option as its raw value; it must go '
|
||||||
|
'through enumOptionLabel() so dynamic rows match server-rendered ones'
|
||||||
|
)
|
||||||
@@ -781,6 +781,20 @@ class TestNewConfigKeys:
|
|||||||
assert cfg.render_width_pct == 100
|
assert cfg.render_width_pct == 100
|
||||||
assert cfg.min_content_separation == 24
|
assert cfg.min_content_separation == 24
|
||||||
|
|
||||||
|
def test_width_cap_is_off_by_default(self):
|
||||||
|
# Capping made wide plugins resume mid-content on every appearance and
|
||||||
|
# emit runt final windows; it is now opt-in per plugin instead.
|
||||||
|
assert VegasModeConfig().max_plugin_width_ratio == 0.0
|
||||||
|
assert VegasModeConfig.from_config({}).max_plugin_width_ratio == 0.0
|
||||||
|
|
||||||
|
def test_width_cap_is_still_available_when_asked_for(self):
|
||||||
|
# Defaulting the cap off must not remove it: a user who sets a ratio
|
||||||
|
# still gets one, and 0 still means uncapped.
|
||||||
|
cfg = VegasModeConfig.from_config(
|
||||||
|
{'display': {'vegas_scroll': {'max_plugin_width_ratio': 3.0}}})
|
||||||
|
assert cfg.max_plugin_width_ratio == 3.0
|
||||||
|
assert cfg.validate() == []
|
||||||
|
|
||||||
@pytest.mark.parametrize('overrides,bad_key', [
|
@pytest.mark.parametrize('overrides,bad_key', [
|
||||||
({'render_width_pct': 5}, 'render_width_pct'),
|
({'render_width_pct': 5}, 'render_width_pct'),
|
||||||
({'render_width_pct': 101}, 'render_width_pct'),
|
({'render_width_pct': 101}, 'render_width_pct'),
|
||||||
|
|||||||
@@ -160,6 +160,24 @@
|
|||||||
|
|
||||||
// ─── Cell rendering ─────────────────────────────────────────────────────
|
// ─── Cell rendering ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visible text for one enum option.
|
||||||
|
*
|
||||||
|
* Mirrors the server-rendered table in plugin_config.html: a schema may
|
||||||
|
* supply x-options.labels, and anything unlabelled falls back to the raw
|
||||||
|
* value. Rows added here must match rows rendered by the template, or the
|
||||||
|
* same column would read differently before and after a page reload.
|
||||||
|
*
|
||||||
|
* @param {Object} colDef column (or property) schema
|
||||||
|
* @param {*} opt the enum value
|
||||||
|
* @returns {string} label to display
|
||||||
|
*/
|
||||||
|
function enumOptionLabel(colDef, opt) {
|
||||||
|
const options = (colDef && (colDef['x-options'] || colDef['x_options'])) || {};
|
||||||
|
const labels = options.labels || {};
|
||||||
|
return Object.prototype.hasOwnProperty.call(labels, opt) ? labels[opt] : opt;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create one <td> for a display column.
|
* Create one <td> for a display column.
|
||||||
*/
|
*/
|
||||||
@@ -219,7 +237,7 @@
|
|||||||
if (opt === null) return;
|
if (opt === null) return;
|
||||||
const o = document.createElement('option');
|
const o = document.createElement('option');
|
||||||
o.value = opt;
|
o.value = opt;
|
||||||
o.textContent = opt;
|
o.textContent = enumOptionLabel(colDef, opt);
|
||||||
if (String(colValue) === String(opt)) o.selected = true;
|
if (String(colValue) === String(opt)) o.selected = true;
|
||||||
sel.appendChild(o);
|
sel.appendChild(o);
|
||||||
});
|
});
|
||||||
@@ -646,7 +664,7 @@
|
|||||||
enumVals.forEach(opt => {
|
enumVals.forEach(opt => {
|
||||||
if (opt === null) return;
|
if (opt === null) return;
|
||||||
const o = document.createElement('option');
|
const o = document.createElement('option');
|
||||||
o.value = opt; o.textContent = opt;
|
o.value = opt; o.textContent = enumOptionLabel(schema, opt);
|
||||||
if (String(currentVal) === String(opt)) o.selected = true;
|
if (String(currentVal) === String(opt)) o.selected = true;
|
||||||
sel.appendChild(o);
|
sel.appendChild(o);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -556,11 +556,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" id="setting-display-vegas_max_plugin_width_ratio" data-setting-key="display.vegas_scroll.max_plugin_width_ratio">
|
<div class="form-group" id="setting-display-vegas_max_plugin_width_ratio" data-setting-key="display.vegas_scroll.max_plugin_width_ratio">
|
||||||
<label for="vegas_max_plugin_width_ratio" class="block text-sm font-medium text-gray-700">Max Plugin Width (screens){{ ui.help_tip('Caps how much of one cycle a single plugin may occupy, measured in screen widths (0–20).\nDefault: 3. A long ticker such as a news feed or leaderboard is trimmed to this and the remainder shown on later cycles, so one plugin cannot hold the display for minutes. Set 0 for no limit.', 'Max Plugin Width') }}</label>
|
<label for="vegas_max_plugin_width_ratio" class="block text-sm font-medium text-gray-700">Max Plugin Width (screens){{ ui.help_tip('Caps how much of one cycle a single plugin may occupy, measured in screen widths (0–20).\nDefault: 0 (no limit) — every plugin shows all of its content and always starts at the beginning.\nSet a limit to stop one long ticker holding the display for minutes: it is cut to this width and the remainder shown on later cycles. The trade-off is that such a plugin then resumes mid-content on each appearance instead of starting fresh.', 'Max Plugin Width') }}</label>
|
||||||
<input type="number"
|
<input type="number"
|
||||||
id="vegas_max_plugin_width_ratio"
|
id="vegas_max_plugin_width_ratio"
|
||||||
name="vegas_max_plugin_width_ratio"
|
name="vegas_max_plugin_width_ratio"
|
||||||
value="{{ main_config.display.get('vegas_scroll', {}).get('max_plugin_width_ratio', 3.0) }}"
|
value="{{ main_config.display.get('vegas_scroll', {}).get('max_plugin_width_ratio', 0.0) }}"
|
||||||
min="0"
|
min="0"
|
||||||
max="20"
|
max="20"
|
||||||
step="0.5"
|
step="0.5"
|
||||||
|
|||||||
@@ -121,14 +121,21 @@
|
|||||||
</label>
|
</label>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{# Enum dropdown #}
|
{# 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 %}
|
{% elif prop.enum %}
|
||||||
|
{% set enum_labels = (prop.get('x-options') or prop.get('x_options') or {}).get('labels') or {} %}
|
||||||
<select id="{{ field_id }}"
|
<select id="{{ field_id }}"
|
||||||
name="{{ full_key }}"
|
name="{{ full_key }}"
|
||||||
class="form-select w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 bg-white text-black">
|
class="form-select w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 bg-white text-black">
|
||||||
{% for option in prop.enum %}
|
{% for option in prop.enum %}
|
||||||
<option value="{{ option }}" {% if value == option %}selected{% endif %}>
|
<option value="{{ option }}" {% if value == option %}selected{% endif %}>
|
||||||
{{ option|replace('_', ' ')|title }}
|
{{ enum_labels.get(option, option|replace('_', ' ')|title) }}
|
||||||
</option>
|
</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
@@ -569,10 +576,14 @@
|
|||||||
class="block w-20 px-2 py-1 border border-gray-300 rounded text-sm text-center"
|
class="block w-20 px-2 py-1 border border-gray-300 rounded text-sm text-center"
|
||||||
{% if col_def.get('description') %}title="{{ col_def.get('description') }}"{% endif %}>
|
{% if col_def.get('description') %}title="{{ col_def.get('description') }}"{% endif %}>
|
||||||
{% elif col_enum %}
|
{% 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 {} %}
|
||||||
<select name="{{ full_key }}.{{ item_index }}.{{ col_name }}"
|
<select name="{{ full_key }}.{{ item_index }}.{{ col_name }}"
|
||||||
class="block w-full px-2 py-1 border border-gray-300 rounded text-sm bg-white">
|
class="block w-full px-2 py-1 border border-gray-300 rounded text-sm bg-white">
|
||||||
{% for opt in col_enum %}{% if opt is not none %}
|
{% for opt in col_enum %}{% if opt is not none %}
|
||||||
<option value="{{ opt }}" {% if col_value == opt or (col_value is none and col_def.get('default') == opt) %}selected{% endif %}>{{ opt }}</option>
|
<option value="{{ opt }}" {% if col_value == opt or (col_value is none and col_def.get('default') == opt) %}selected{% endif %}>{{ col_labels.get(opt, opt) }}</option>
|
||||||
{% endif %}{% endfor %}
|
{% endif %}{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
{% elif col_xwidget == 'date-picker' %}
|
{% elif col_xwidget == 'date-picker' %}
|
||||||
|
|||||||
Reference in New Issue
Block a user