Making more progress on dynamic widget stuff.
This commit is contained in:
parent
b2231f8ef9
commit
fc275c6e5d
5 changed files with 107 additions and 107 deletions
|
|
@ -1,104 +1,84 @@
|
|||
{% import "fragments/_icon_fragment.html" as icons %}
|
||||
|
||||
{% macro render_combobox(id, options, label=none, placeholder=none, onAdd=none, onRemove=none, onEdit=none, data_attributes=none) %}
|
||||
<!-- Combobox Widget Fragment -->
|
||||
{% macro render_combobox(id, options, label=none, placeholder=none, onAdd=none, onRemove=none, onEdit=none,
|
||||
data_attributes=none) %}
|
||||
<!-- Combobox Widget Fragment -->
|
||||
|
||||
{% if label %}
|
||||
<label for="{{ id }}-input" class="form-label">{{ label }}</label>
|
||||
{% endif %}
|
||||
<div class="combo-box-widget" id="{{ id }}-container">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control rounded-bottom-0" id="{{ id }}-input"{% if placeholder %} placeholder="{{ placeholder }}"{% endif %}>
|
||||
<button type="button" class="btn btn-primary rounded-bottom-0" id="{{ id }}-add" disabled>
|
||||
{{ icons.render_icon('plus-lg', 16, 'icon-state') }}
|
||||
</button>
|
||||
<button type="button" class="btn btn-danger rounded-bottom-0" id="{{ id }}-remove" disabled>
|
||||
{{ icons.render_icon('dash-lg', 16) }}
|
||||
</button>
|
||||
</div>
|
||||
<select class="form-select border-top-0 rounded-top-0" id="{{ id }}-list" name="{{ id }}" size="10" multiple>
|
||||
{% for option in options %}
|
||||
<option value="{{ option.id }}"
|
||||
{% if data_attributes %}
|
||||
{% for key, data_attr in data_attributes.items() %}
|
||||
{% if option[key] is defined %}
|
||||
data-{{ data_attr }}="{{ option[key] }}"
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}>
|
||||
{{ option.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% if label %}
|
||||
<label for="{{ id }}-input" class="form-label">{{ label }}</label>
|
||||
{% endif %}
|
||||
<div class="combo-box-widget" id="{{ id }}-container">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control rounded-bottom-0" id="{{ id }}-input" {% if placeholder %}
|
||||
placeholder="{{ placeholder }}" {% endif %}>
|
||||
<button type="button" class="btn btn-primary rounded-bottom-0" id="{{ id }}-add" disabled>
|
||||
{{ icons.render_icon('plus-lg', 16, 'icon-state') }}
|
||||
</button>
|
||||
<button type="button" class="btn btn-danger rounded-bottom-0" id="{{ id }}-remove" disabled>
|
||||
{{ icons.render_icon('dash-lg', 16) }}
|
||||
</button>
|
||||
</div>
|
||||
<select class="form-select border-top-0 rounded-top-0" id="{{ id }}-list" name="{{ id }}" size="10" multiple>
|
||||
{% for option in options %}
|
||||
<option value="{{ option.id }}" {% if data_attributes %} {% for key, data_attr in data_attributes.items() %} {%
|
||||
if option[key] is defined %} data-{{ data_attr }}="{{ option[key] }}" {% endif %} {% endfor %} {% endif %}>
|
||||
{{ option.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
ComboBoxWidget.initComboBox("{{ id }}"{% if onAdd or onRemove or onEdit %}, {
|
||||
{% if onAdd %}onAdd: function(newItem, list, createOption) { {{ onAdd | safe }} },{% endif %}
|
||||
{% if onRemove %}onRemove: function(option) { {{ onRemove | safe }} },{% endif %}
|
||||
{% if onEdit %}onEdit: function(option) { {{ onEdit | safe }} }{% endif %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
ComboBoxWidget.initComboBox("{{ id }}"{% if onAdd or onRemove or onEdit %}, {
|
||||
{% if onAdd %}onAdd: function(newItem, list, createOption) { { { onAdd | safe } } }, {% endif %}
|
||||
{% if onRemove %} onRemove: function(option) { { { onRemove | safe } } }, {% endif %}
|
||||
{% if onEdit %} onEdit: function(option) { { { onEdit | safe } } } {% endif %}
|
||||
}{% endif %});
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro dynamic_combobox(
|
||||
id, options, label = none, placeholder = none, data_attributes = none,
|
||||
create_url = none, edit_url = none, delete_url = none, refresh_url = none
|
||||
id, options, label = none, placeholder = none, data_attributes = none,
|
||||
create_url = none, edit_url = none, delete_url = none, refresh_url = none
|
||||
) %}
|
||||
{% if label %}
|
||||
<label for="{{ id }}-input" class="form-label">{{ label }}</label>
|
||||
{% endif %}
|
||||
<div id="{{ id }}-container"
|
||||
x-data="ComboBox({
|
||||
id: '{{ id }}',
|
||||
createUrl: {{ create_url|tojson if create_url else 'null' }},
|
||||
editUrl: {{ edit_url|tojson if edit_url else 'null' }},
|
||||
deleteUrl: {{ delete_url|tojson if delete_url else 'null' }},
|
||||
refreshUrl: {{ refresh_url|tojson if refresh_url else 'null' }},
|
||||
})"
|
||||
hx-preserve
|
||||
class="combo-box-widget">
|
||||
<div class="input-group">
|
||||
<input type="text"
|
||||
id="{{ id }}-input"
|
||||
x-model.trim="query"
|
||||
@keydown.enter.prevent="submitAddOrEdit()"
|
||||
@keydown.escape="cancelEdit()"
|
||||
class="form-control rounded-bottom-0">
|
||||
{% if label %}
|
||||
<label for="{{ id }}-input" class="form-label">{{ label }}</label>
|
||||
{% endif %}
|
||||
<div id="{{ id }}-container" x-data='ComboBox({
|
||||
id: {{ id|tojson }},
|
||||
createUrl: {{ create_url|tojson if create_url else "null" }},
|
||||
editUrl: {{ edit_url|tojson if edit_url else "null" }},
|
||||
deleteUrl: {{ delete_url|tojson if delete_url else "null" }},
|
||||
refreshUrl: {{ refresh_url|tojson if refresh_url else "null" }}
|
||||
})' hx-preserve class="combo-box-widget">
|
||||
<div class="input-group">
|
||||
<input type="text" id="{{ id }}-input" x-model.trim="query" @keydown.enter.prevent="submitAddOrEdit()"
|
||||
@keydown.escape="cancelEdit()" class="form-control rounded-bottom-0">
|
||||
|
||||
<button id="{{ id }}-add"
|
||||
:disabled="!query"
|
||||
@click="submitAddOrEdit()"
|
||||
class="btn btn-primary rounded-bottom-0">
|
||||
<i class="bi icon-state" :class="isEditing ? 'bi-pencil' : 'bi-plus-lg'"></i>
|
||||
</button>
|
||||
<button id="{{ id }}-add" :disabled="!query" @click="submitAddOrEdit()"
|
||||
class="btn btn-primary rounded-bottom-0">
|
||||
<i class="bi icon-state" :class="isEditing ? 'bi-pencil' : 'bi-plus-lg'"></i>
|
||||
</button>
|
||||
|
||||
<button id="{{ id }}-remove"
|
||||
:disabled="!hasSelection"
|
||||
@click="removeSelected()"
|
||||
class="btn btn-danger rounded-bottom-0">
|
||||
{{ icons.render_icon('dash-lg', 16) }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<select id="{{ id }}-list" multiple
|
||||
x-ref="list"
|
||||
@change="onListChange"
|
||||
class="form-select border-top-0 rounded-top-0"
|
||||
name="{{ id }}" size="10">
|
||||
{% for option in options %}
|
||||
<option value="{{ option.id }}">{{ option.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
{% if refresh_url %}
|
||||
<div id="{{ id }}-htmx-refresh" class="d-none"
|
||||
hx-get="{{ refresh_url }}"
|
||||
hx-trigger="combobox:refresh from:#{{ id }}-container"
|
||||
hx-target="#{{ id }}-list"
|
||||
hx-swap="innerHTML"></div>
|
||||
{% endif %}
|
||||
<button id="{{ id }}-remove" :disabled="!hasSelection" @click="removeSelected()"
|
||||
class="btn btn-danger rounded-bottom-0">
|
||||
{{ icons.render_icon('dash-lg', 16) }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<select id="{{ id }}-list" multiple x-ref="list" @change="onListChange"
|
||||
class="form-select border-top-0 rounded-top-0" name="{{ id }}" size="10">
|
||||
{% for option in options if options %}
|
||||
<option value="{{ option.id }}">{{ option.name }}</option>
|
||||
{% endfor %}
|
||||
<option disabled>Loading...</option>
|
||||
</select>
|
||||
|
||||
{% if refresh_url %}
|
||||
<div id="{{ id }}-htmx-refresh" class="d-none" hx-get="{{ refresh_url }}"
|
||||
hx-trigger="load, combobox:refresh from:#{{ id }}-container" hx-target="#{{ id }}-list" hx-swap="innerHTML"></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
6
inventory/templates/fragments/_option_fragment.html
Normal file
6
inventory/templates/fragments/_option_fragment.html
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{% for option in options %}
|
||||
<option value="{{ option.id }}"
|
||||
{% for k,v in option.items() if k not in ('id','name') and v is not none %}
|
||||
data-{{ k }}="{{ v }}"
|
||||
{% endfor %}>{{ option.name }}</option>
|
||||
{% endfor %}
|
||||
|
|
@ -2,17 +2,8 @@
|
|||
|
||||
{% block content %}
|
||||
{{ combos.dynamic_combobox(
|
||||
'play',
|
||||
[
|
||||
{
|
||||
'id': 1,
|
||||
'name': 'Beans'
|
||||
},
|
||||
{
|
||||
'id': 2,
|
||||
'name': 'Toast'
|
||||
},
|
||||
],
|
||||
'Breakfast!'
|
||||
id='play',
|
||||
label='Breakfast!',
|
||||
refresh_url=url_for('ui.list_items', model_name='brand', view='option')
|
||||
) }}
|
||||
{% endblock %}
|
||||
|
|
@ -50,7 +50,9 @@ def list_items(model_name):
|
|||
|
||||
rows = call(Model, "ui_query", db.session, text=text, limit=limit, offset=offset) \
|
||||
or default_query(db.session, Model, text=text, limit=limit, offset=offset)
|
||||
items = [call(Model, 'ui_serialize', r, view=view) or default_serialize(Model, r, view=view)
|
||||
for r in rows]
|
||||
|
||||
data = [ (call(Model, "ui_serialize", r, view=view) or default_serialize(Model, r, view=view))
|
||||
for r in rows ]
|
||||
return jsonify({"items": data})
|
||||
if view=="option":
|
||||
return render_template('fragments/_option_fragment.html', options=items)
|
||||
return jsonify({"items": items})
|
||||
|
|
|
|||
|
|
@ -89,9 +89,30 @@ def default_delete(session, Model, ids):
|
|||
return count
|
||||
|
||||
def default_serialize(Model, obj, *, view='option'):
|
||||
label = infer_label_attr(Model)
|
||||
data = {'id': obj.id, 'name': getattr(obj, label)}
|
||||
# 1. Explicit config wins
|
||||
label_attr = getattr(Model, 'ui_label_attr', None)
|
||||
|
||||
# 2. Otherwise, pick the first PREFERRED_LABELS that exists (can be @property or real column)
|
||||
if not label_attr:
|
||||
for candidate in PREFERRED_LABELS:
|
||||
if hasattr(obj, candidate):
|
||||
label_attr = candidate
|
||||
break
|
||||
|
||||
# 3. Fallback to str(obj) if literally nothing found
|
||||
if not label_attr:
|
||||
name_val = str(obj)
|
||||
else:
|
||||
try:
|
||||
name_val = getattr(obj, label_attr)
|
||||
except Exception:
|
||||
name_val = str(obj)
|
||||
|
||||
data = {'id': obj.id, 'name': name_val}
|
||||
|
||||
# Include extra attrs if defined
|
||||
for attr in getattr(Model, 'ui_extra_attrs', ()):
|
||||
if hasattr(obj, attr):
|
||||
data[attr] = getattr(obj, attr)
|
||||
|
||||
return data
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue