add table is better now

This commit is contained in:
Priec
2026-08-03 14:44:12 +02:00
parent c8afe99d79
commit 023f8c9dcb
11 changed files with 1799 additions and 212 deletions

View File

@@ -1,42 +1,24 @@
{# GET /admin/tables/new — crate::pages::add_table::ui::AddTablePage #}
{% extends "ui/form_page.html" %}
{% import "ui/alert.html" as alert %}
{% block title %}Add table{% endblock %}
{% block eyebrow %}Table definition{% endblock %}
{% block heading %}Add table{% endblock %}
{% block lead %}<p>Create a table through the existing gRPC table-definition service.</p>{% endblock %}
{% block lead %}<p>Build the table one column at a time, the same way the terminal client does.</p>{% endblock %}
{% block form %}
<form hx-post="/admin/tables" hx-target="#submission-status" hx-swap="innerHTML"
{#
One form holds the whole draft. `#builder` is what every interaction swaps,
so the form element itself — and therefore the submit target — survives.
#}
<form id="table-form" hx-post="/admin/tables" hx-target="#builder" hx-swap="innerHTML"
hx-disabled-elt="button[type=submit]">
<div class="form-grid">
<label>Profile
<select name="profile_name" required>
<option value="">Choose a profile</option>
{% for profile in page.profiles %}
<option value="{{ profile }}" {% if page.form.profile_name == *profile %}selected{% endif %}>{{ profile }}</option>
{% endfor %}
</select>
</label>
<label>Table name<input name="table_name" value="{{ page.form.table_name }}" required placeholder="invoices"></label>
<label class="wide">Columns
<textarea name="columns" rows="9" required
placeholder="number: text:indexed&#10;issued_on: date&#10;stock: int:quantity-ledger">{{ page.form.columns }}</textarea>
<small>One per line: <code>name: type: optional flags</code>. Flags: indexed, half-up, quantity-ledger.</small>
</label>
<label>Additional indexed columns<input name="indexed_columns" value="{{ page.form.indexed_columns }}" placeholder="number, issued_on"></label>
<label>Base currency<input name="base_currency" value="{{ page.form.base_currency }}" maxlength="3" placeholder="EUR"></label>
<label>Required links<input name="required_links" value="{{ page.form.required_links }}" placeholder="customer, address"></label>
<label>Optional links<input name="optional_links" value="{{ page.form.optional_links }}" placeholder="project"></label>
<label>Row display columns<input name="row_display_columns" value="{{ page.form.row_display_columns }}" placeholder="name, ico"></label>
</div>
<div id="submission-status" aria-live="polite">
{%- if let Some(message) = page.error %}{% call alert::error("Could not create the table", message) %}{% endcall %}{% endif -%}
</div>
<div class="form-actions">
<a href="/admin">Cancel</a>
<button type="submit">Create table</button>
<div id="builder" aria-live="polite">
{% include "pages/add_table/builder.html" %}
</div>
</form>
<datalist id="currency-codes">
{% for code in currency_codes %}<option value="{{ code }}"></option>{% endfor %}
</datalist>
{% endblock %}

View File

@@ -0,0 +1,237 @@
{#
POST /admin/tables/builder — crate::pages::add_table::ui::BuilderFragment.
The whole builder, which is also what `add_table.html` embeds on first load.
Every control posts the entire draft back to /admin/tables/builder and swaps
this markup in again, so the rules live on the server exactly as they do in
the TUI client. The hidden inputs in the columns, relations and display
sections are what carry the draft across requests.
#}
{% import "ui/alert.html" as alert %}
{%- if let Some(message) = page.error %}{% call alert::error("Could not continue", message) %}{% endcall %}{% endif -%}
{%- if let Some(message) = page.status %}{% call alert::success("Draft updated", message) %}{% endcall %}{% endif -%}
<section class="builder-section">
<h2>Table</h2>
<div class="form-grid">
<label>Profile
<select name="profile_name" hx-post="/admin/tables/builder" hx-trigger="change"
hx-include="#table-form" hx-target="#builder" hx-swap="innerHTML"
hx-vals='{"action": "refresh"}'>
<option value="">Choose a profile</option>
{% for profile in page.profiles %}
<option value="{{ profile }}" {% if page.selected_profile() == profile.as_str() %}selected{% endif %}>{{ profile }}</option>
{% endfor %}
<option value="__new__" {% if page.draft.creating_new_profile %}selected{% endif %}>+ New profile…</option>
</select>
</label>
{% if page.draft.show_profile_name_input() %}
<label>New profile name
<input name="profile_name_input" value="{{ page.draft.profile_name_input }}" placeholder="bookkeeping">
</label>
{% endif %}
{% if page.draft.show_accounting_currency() %}
<label>Accounting currency
<input name="accounting_currency" value="{{ page.draft.accounting_currency }}" list="currency-codes"
maxlength="3" placeholder="EUR">
</label>
{% else %}
<input type="hidden" name="accounting_currency" value="{{ page.draft.accounting_currency }}">
{% endif %}
<label>Table name
<input name="table_name" value="{{ page.draft.table_name }}" placeholder="invoice"
hx-post="/admin/tables/builder" hx-trigger="change" hx-include="#table-form"
hx-target="#builder" hx-swap="innerHTML" hx-vals='{"action": "refresh"}'>
</label>
{% if page.draft.show_base_currency() %}
<label>Base currency
<input name="base_currency" value="{{ page.draft.base_currency }}" list="currency-codes"
maxlength="3" placeholder="EUR">
<small>Required while the table has a MONEY or ACCOUNTING column.</small>
</label>
{% else %}
<input type="hidden" name="base_currency" value="{{ page.draft.base_currency }}">
{% endif %}
</div>
</section>
<section class="builder-section">
<h2>Add a column</h2>
<div class="form-grid">
<label>Column name
<input name="column_name_input" value="{{ page.draft.column_name_input }}" placeholder="number">
</label>
<label>Column type
<select name="column_type_input" hx-post="/admin/tables/builder" hx-trigger="change"
hx-include="#table-form" hx-target="#builder" hx-swap="innerHTML"
hx-vals='{"action": "refresh"}'>
<option value="">Choose a type</option>
{% for column_type in column_types %}
<option value="{{ column_type }}" {% if page.draft.column_type_input == *column_type %}selected{% endif %}>{{ column_type }}</option>
{% endfor %}
</select>
</label>
{% if page.draft.show_temporal_type() %}
<label>Temporal type
<select name="temporal_type_input">
<option value="">Choose a temporal type</option>
{% for temporal_type in temporal_types %}
<option value="{{ temporal_type }}" {% if page.draft.temporal_type_input == *temporal_type %}selected{% endif %}>{{ temporal_type }}</option>
{% endfor %}
</select>
</label>
{% endif %}
{% if page.draft.show_gtin_type() %}
<label>GTIN type
<select name="gtin_type_input">
<option value="">Choose a GTIN length</option>
{% for gtin_type in gtin_types %}
<option value="{{ gtin_type }}" {% if page.draft.gtin_type_input == *gtin_type %}selected{% endif %}>GTIN-{{ gtin_type }}</option>
{% endfor %}
</select>
</label>
{% endif %}
{% if page.draft.show_rounding() %}
<label>Rounding
<select name="column_rounding_input">
<option value="none" {% if page.draft.column_rounding_input != "half-up" %}selected{% endif %}>none</option>
<option value="half-up" {% if page.draft.column_rounding_input == "half-up" %}selected{% endif %}>half-up</option>
</select>
</label>
{% endif %}
<label>Indexing
<select name="column_indexing_input">
<option value="no" {% if page.draft.column_indexing_input != "yes" %}selected{% endif %}>no</option>
<option value="yes" {% if page.draft.column_indexing_input == "yes" %}selected{% endif %}>yes</option>
</select>
</label>
<label>Quantity ledger
<select name="column_quantity_ledger_input">
<option value="no" {% if page.draft.column_quantity_ledger_input != "yes" %}selected{% endif %}>no</option>
<option value="yes" {% if page.draft.column_quantity_ledger_input == "yes" %}selected{% endif %}>yes</option>
</select>
<small>INT, BIGINT, DECIMAL or MONEY only.</small>
</label>
</div>
<button type="button" class="secondary" hx-post="/admin/tables/builder" hx-include="#table-form"
hx-target="#builder" hx-swap="innerHTML" hx-vals='{"action": "add-column"}'>Add column</button>
</section>
<section class="builder-section">
<h2>Columns <span class="count">{{ page.draft.columns.len() }}</span></h2>
{% if page.draft.columns.is_empty() %}
<p class="hint">No columns yet. Describe one above and press <em>Add column</em>.</p>
{% else %}
<table class="builder-table">
<thead><tr><th>Name</th><th>Type</th><th>Indexed</th><th>Options</th><th></th></tr></thead>
<tbody>
{% for column in page.draft.columns %}
<tr>
<td><code>{{ column.name }}</code></td>
<td>{{ column.data_type }}</td>
<td>
<button type="button" class="toggle" hx-post="/admin/tables/builder" hx-include="#table-form"
hx-target="#builder" hx-swap="innerHTML"
hx-vals='{"action": "toggle-index", "index": "{{ loop.index0 }}"}'>
{% if column.indexed %}[x]{% else %}[ ]{% endif %}
</button>
</td>
<td>
{% if column.quantity_ledger %}<span class="tag">quantity ledger</span>{% endif %}
{% if !column.option_label().is_empty() %}<span class="tag">{{ column.option_label() }}</span>{% endif %}
</td>
<td>
<button type="button" class="danger" hx-post="/admin/tables/builder" hx-include="#table-form"
hx-target="#builder" hx-swap="innerHTML"
hx-vals='{"action": "remove-column", "index": "{{ loop.index0 }}"}'>Remove</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{# The draft itself: one set of fields per column, in order. #}
{% for column in page.draft.columns %}
<input type="hidden" name="column_names" value="{{ column.name }}">
<input type="hidden" name="column_types" value="{{ column.data_type }}">
<input type="hidden" name="column_indexed" value="{% if column.indexed %}yes{% else %}no{% endif %}">
<input type="hidden" name="column_quantity_ledger" value="{% if column.quantity_ledger %}yes{% else %}no{% endif %}">
<input type="hidden" name="column_rounding" value="{{ column.money_mode.label() }}">
{% endfor %}
</section>
<section class="builder-section">
<h2>Relations</h2>
{% if page.draft.links.is_empty() %}
<p class="hint">No other tables in this profile to link to.</p>
{% else %}
<p class="hint">Each active relation adds a <code>&lt;table&gt;_id</code> column.</p>
<ul class="builder-list">
{% for link in page.draft.links %}
<li>
<button type="button" class="toggle mode-{{ link.mode.label() }}" hx-post="/admin/tables/builder"
hx-include="#table-form" hx-target="#builder" hx-swap="innerHTML"
hx-vals='{"action": "cycle-link", "index": "{{ loop.index0 }}"}'>{{ link.mode.label() }}</button>
<code>{{ link.linked_table_name }}</code>
</li>
{% endfor %}
</ul>
{% endif %}
{% for link in page.draft.links %}
<input type="hidden" name="link_tables" value="{{ link.linked_table_name }}">
<input type="hidden" name="link_modes" value="{{ link.mode.label() }}">
{% endfor %}
</section>
<section class="builder-section">
<h2>Row display columns</h2>
<p class="hint">What identifies a row to users, in the order chosen. Pick <code>id</code> to clear the list.</p>
<ul class="builder-list">
{% for candidate in page.row_display_candidates() %}
<li>
<button type="button" class="toggle" hx-post="/admin/tables/builder" hx-include="#table-form"
hx-target="#builder" hx-swap="innerHTML"
hx-vals='{"action": "toggle-display", "index": "{{ candidate.index }}"}'>{{ candidate.mark() }}</button>
<code>{{ candidate.name }}</code>
</li>
{% endfor %}
</ul>
{% for display in page.draft.row_display_columns %}
<input type="hidden" name="row_display_columns" value="{{ display }}">
{% endfor %}
</section>
<section class="builder-section">
<h2>Table definition preview</h2>
<table class="builder-table preview">
<thead><tr><th></th><th>Column</th><th>Type</th><th>Options</th><th>Source</th></tr></thead>
<tbody>
{% for row in page.draft.preview_rows() %}
<tr class="source-{{ row.source }}">
<td class="mark">{{ row.mark }}</td>
<td><code>{{ row.column }}</code></td>
<td>{{ row.data_type }}</td>
<td>{{ row.option }}</td>
<td>{{ row.source }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</section>
<div class="form-actions">
<a href="/admin">Cancel</a>
<button type="submit">Create table</button>
</div>