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,14 +1,16 @@
|
||||||
{% import "fragments/_icon_fragment.html" as icons %}
|
{% 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) %}
|
{% macro render_combobox(id, options, label=none, placeholder=none, onAdd=none, onRemove=none, onEdit=none,
|
||||||
<!-- Combobox Widget Fragment -->
|
data_attributes=none) %}
|
||||||
|
<!-- Combobox Widget Fragment -->
|
||||||
|
|
||||||
{% if label %}
|
{% if label %}
|
||||||
<label for="{{ id }}-input" class="form-label">{{ label }}</label>
|
<label for="{{ id }}-input" class="form-label">{{ label }}</label>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="combo-box-widget" id="{{ id }}-container">
|
<div class="combo-box-widget" id="{{ id }}-container">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text" class="form-control rounded-bottom-0" id="{{ id }}-input"{% if placeholder %} placeholder="{{ placeholder }}"{% endif %}>
|
<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>
|
<button type="button" class="btn btn-primary rounded-bottom-0" id="{{ id }}-add" disabled>
|
||||||
{{ icons.render_icon('plus-lg', 16, 'icon-state') }}
|
{{ icons.render_icon('plus-lg', 16, 'icon-state') }}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -18,87 +20,65 @@
|
||||||
</div>
|
</div>
|
||||||
<select class="form-select border-top-0 rounded-top-0" id="{{ id }}-list" name="{{ id }}" size="10" multiple>
|
<select class="form-select border-top-0 rounded-top-0" id="{{ id }}-list" name="{{ id }}" size="10" multiple>
|
||||||
{% for option in options %}
|
{% for option in options %}
|
||||||
<option value="{{ option.id }}"
|
<option value="{{ option.id }}" {% if data_attributes %} {% for key, data_attr in data_attributes.items() %} {%
|
||||||
{% if data_attributes %}
|
if option[key] is defined %} data-{{ data_attr }}="{{ option[key] }}" {% endif %} {% endfor %} {% endif %}>
|
||||||
{% for key, data_attr in data_attributes.items() %}
|
|
||||||
{% if option[key] is defined %}
|
|
||||||
data-{{ data_attr }}="{{ option[key] }}"
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}>
|
|
||||||
{{ option.name }}
|
{{ option.name }}
|
||||||
</option>
|
</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
ComboBoxWidget.initComboBox("{{ id }}"{% if onAdd or onRemove or onEdit %}, {
|
ComboBoxWidget.initComboBox("{{ id }}"{% if onAdd or onRemove or onEdit %}, {
|
||||||
{% if onAdd %}onAdd: function(newItem, list, createOption) { {{ onAdd | safe }} },{% endif %}
|
{% if onAdd %}onAdd: function(newItem, list, createOption) { { { onAdd | safe } } }, {% endif %}
|
||||||
{% if onRemove %}onRemove: function(option) { {{ onRemove | safe }} },{% endif %}
|
{% if onRemove %} onRemove: function(option) { { { onRemove | safe } } }, {% endif %}
|
||||||
{% if onEdit %}onEdit: function(option) { {{ onEdit | safe }} }{% endif %}
|
{% if onEdit %} onEdit: function(option) { { { onEdit | safe } } } {% endif %}
|
||||||
}{% endif %});
|
}{% endif %});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro dynamic_combobox(
|
{% macro dynamic_combobox(
|
||||||
id, options, label = none, placeholder = none, data_attributes = none,
|
id, options, label = none, placeholder = none, data_attributes = none,
|
||||||
create_url = none, edit_url = none, delete_url = none, refresh_url = none
|
create_url = none, edit_url = none, delete_url = none, refresh_url = none
|
||||||
) %}
|
) %}
|
||||||
{% if label %}
|
{% if label %}
|
||||||
<label for="{{ id }}-input" class="form-label">{{ label }}</label>
|
<label for="{{ id }}-input" class="form-label">{{ label }}</label>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div id="{{ id }}-container"
|
<div id="{{ id }}-container" x-data='ComboBox({
|
||||||
x-data="ComboBox({
|
id: {{ id|tojson }},
|
||||||
id: '{{ id }}',
|
createUrl: {{ create_url|tojson if create_url else "null" }},
|
||||||
createUrl: {{ create_url|tojson if create_url else 'null' }},
|
editUrl: {{ edit_url|tojson if edit_url else "null" }},
|
||||||
editUrl: {{ edit_url|tojson if edit_url else 'null' }},
|
deleteUrl: {{ delete_url|tojson if delete_url else "null" }},
|
||||||
deleteUrl: {{ delete_url|tojson if delete_url else 'null' }},
|
refreshUrl: {{ refresh_url|tojson if refresh_url else "null" }}
|
||||||
refreshUrl: {{ refresh_url|tojson if refresh_url else 'null' }},
|
})' hx-preserve class="combo-box-widget">
|
||||||
})"
|
|
||||||
hx-preserve
|
|
||||||
class="combo-box-widget">
|
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text"
|
<input type="text" id="{{ id }}-input" x-model.trim="query" @keydown.enter.prevent="submitAddOrEdit()"
|
||||||
id="{{ id }}-input"
|
@keydown.escape="cancelEdit()" class="form-control rounded-bottom-0">
|
||||||
x-model.trim="query"
|
|
||||||
@keydown.enter.prevent="submitAddOrEdit()"
|
|
||||||
@keydown.escape="cancelEdit()"
|
|
||||||
class="form-control rounded-bottom-0">
|
|
||||||
|
|
||||||
<button id="{{ id }}-add"
|
<button id="{{ id }}-add" :disabled="!query" @click="submitAddOrEdit()"
|
||||||
:disabled="!query"
|
|
||||||
@click="submitAddOrEdit()"
|
|
||||||
class="btn btn-primary rounded-bottom-0">
|
class="btn btn-primary rounded-bottom-0">
|
||||||
<i class="bi icon-state" :class="isEditing ? 'bi-pencil' : 'bi-plus-lg'"></i>
|
<i class="bi icon-state" :class="isEditing ? 'bi-pencil' : 'bi-plus-lg'"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button id="{{ id }}-remove"
|
<button id="{{ id }}-remove" :disabled="!hasSelection" @click="removeSelected()"
|
||||||
:disabled="!hasSelection"
|
|
||||||
@click="removeSelected()"
|
|
||||||
class="btn btn-danger rounded-bottom-0">
|
class="btn btn-danger rounded-bottom-0">
|
||||||
{{ icons.render_icon('dash-lg', 16) }}
|
{{ icons.render_icon('dash-lg', 16) }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<select id="{{ id }}-list" multiple
|
<select id="{{ id }}-list" multiple x-ref="list" @change="onListChange"
|
||||||
x-ref="list"
|
class="form-select border-top-0 rounded-top-0" name="{{ id }}" size="10">
|
||||||
@change="onListChange"
|
{% for option in options if options %}
|
||||||
class="form-select border-top-0 rounded-top-0"
|
|
||||||
name="{{ id }}" size="10">
|
|
||||||
{% for option in options %}
|
|
||||||
<option value="{{ option.id }}">{{ option.name }}</option>
|
<option value="{{ option.id }}">{{ option.name }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
<option disabled>Loading...</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
{% if refresh_url %}
|
{% if refresh_url %}
|
||||||
<div id="{{ id }}-htmx-refresh" class="d-none"
|
<div id="{{ id }}-htmx-refresh" class="d-none" hx-get="{{ refresh_url }}"
|
||||||
hx-get="{{ refresh_url }}"
|
hx-trigger="load, combobox:refresh from:#{{ id }}-container" hx-target="#{{ id }}-list" hx-swap="innerHTML"></div>
|
||||||
hx-trigger="combobox:refresh from:#{{ id }}-container"
|
|
||||||
hx-target="#{{ id }}-list"
|
|
||||||
hx-swap="innerHTML"></div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endmacro %}
|
{% 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 %}
|
{% block content %}
|
||||||
{{ combos.dynamic_combobox(
|
{{ combos.dynamic_combobox(
|
||||||
'play',
|
id='play',
|
||||||
[
|
label='Breakfast!',
|
||||||
{
|
refresh_url=url_for('ui.list_items', model_name='brand', view='option')
|
||||||
'id': 1,
|
|
||||||
'name': 'Beans'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': 2,
|
|
||||||
'name': 'Toast'
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'Breakfast!'
|
|
||||||
) }}
|
) }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
@ -50,7 +50,9 @@ def list_items(model_name):
|
||||||
|
|
||||||
rows = call(Model, "ui_query", db.session, text=text, limit=limit, offset=offset) \
|
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)
|
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))
|
if view=="option":
|
||||||
for r in rows ]
|
return render_template('fragments/_option_fragment.html', options=items)
|
||||||
return jsonify({"items": data})
|
return jsonify({"items": items})
|
||||||
|
|
|
||||||
|
|
@ -89,9 +89,30 @@ def default_delete(session, Model, ids):
|
||||||
return count
|
return count
|
||||||
|
|
||||||
def default_serialize(Model, obj, *, view='option'):
|
def default_serialize(Model, obj, *, view='option'):
|
||||||
label = infer_label_attr(Model)
|
# 1. Explicit config wins
|
||||||
data = {'id': obj.id, 'name': getattr(obj, label)}
|
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', ()):
|
for attr in getattr(Model, 'ui_extra_attrs', ()):
|
||||||
if hasattr(obj, attr):
|
if hasattr(obj, attr):
|
||||||
data[attr] = getattr(obj, attr)
|
data[attr] = getattr(obj, attr)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue