web interface for table_definition improved
This commit is contained in:
303
web/templates/pages/admin/table_definition/workspace.html
Normal file
303
web/templates/pages/admin/table_definition/workspace.html
Normal file
@@ -0,0 +1,303 @@
|
||||
{#
|
||||
The whole workspace — crate::pages::admin::table_definition::ui::WorkspaceFragment,
|
||||
and what table_definition.html embeds on first load.
|
||||
|
||||
Which panels appear follows how much has been selected: the copy and invoice
|
||||
template panels need a profile, everything else needs a table, and a
|
||||
backend-managed ("system") table gets none of the write panels at all,
|
||||
because the server refuses every one of them for it.
|
||||
#}
|
||||
{% import "ui/alert.html" as alert %}
|
||||
{% import "ui/dialog.html" as dialog %}
|
||||
|
||||
{%- if let Some(message) = page.error %}{% call alert::error("Could not continue", message) %}{% endcall %}{% endif -%}
|
||||
{%- if let Some(message) = page.status %}{% call alert::success("Done", message) %}{% endcall %}{% endif -%}
|
||||
{%- if let Some(message) = page.error %}{% call dialog::error("Could not continue", message) %}{% endcall %}{% endif -%}
|
||||
|
||||
{% if let Some(sql) = page.sql %}
|
||||
{% if !sql.is_empty() %}
|
||||
<section class="panel">
|
||||
<h2>SQL the backend ran</h2>
|
||||
<pre class="sql-preview">{{ sql }}</pre>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if !page.generated.is_empty() %}
|
||||
<section class="panel">
|
||||
<h2>Tables created from the template</h2>
|
||||
<table class="builder-table">
|
||||
<thead><tr><th>Table</th><th>Collection</th><th>Parent</th></tr></thead>
|
||||
<tbody>
|
||||
{% for generated in page.generated %}
|
||||
<tr>
|
||||
<td><code>{{ generated.table_name }}</code></td>
|
||||
<td>{% if generated.collection_path.is_empty() %}<span class="hint">root</span>{% else %}<code>{{ generated.collection_path }}</code>{% endif %}</td>
|
||||
<td>{% if generated.parent_table_name.is_empty() %}<span class="hint">—</span>{% else %}<code>{{ generated.parent_table_name }}</code>{% endif %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
<section class="panel">
|
||||
<h2>Selection</h2>
|
||||
<div class="form-grid">
|
||||
<label>Profile
|
||||
<select name="profile" hx-get="/admin/table-definition/workspace"
|
||||
hx-target="#table-definition-workspace" hx-swap="innerHTML">
|
||||
<option value="">Choose a profile</option>
|
||||
{% for profile in page.profiles %}
|
||||
<option value="{{ profile }}" {% if page.selection.profile == *profile %}selected{% endif %}>{{ profile }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
{% if page.selection.has_profile() %}
|
||||
<label>Table
|
||||
<select name="table" hx-get="/admin/table-definition/workspace"
|
||||
hx-target="#table-definition-workspace" hx-swap="innerHTML"
|
||||
hx-include="[name='profile']">
|
||||
<option value="">Choose a table</option>
|
||||
{% for table in page.tables %}
|
||||
<option value="{{ table.name }}" {% if page.selection.table == table.name %}selected{% endif %}>
|
||||
{{ table.name }}{% if table.is_system() %} (system){% endif %}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if page.selection.has_profile() %}
|
||||
<div class="actions panel-actions">
|
||||
<a href="/admin/tables/new?profile={{ page.selection.profile }}">+ Create a table in this profile</a>
|
||||
</div>
|
||||
|
||||
{% if page.tables.is_empty() %}
|
||||
<p class="hint">This profile has no tables yet.</p>
|
||||
{% else %}
|
||||
<table class="builder-table">
|
||||
<thead><tr><th>Table</th><th>Kind</th><th>Depends on</th><th>Row display</th></tr></thead>
|
||||
<tbody>
|
||||
{% for table in page.tables %}
|
||||
<tr>
|
||||
<td><code>{{ table.name }}</code></td>
|
||||
<td>{{ table.table_kind }}</td>
|
||||
<td>{% if table.depends_on.is_empty() %}<span class="hint">—</span>{% else %}{{ table.depends_on|join(", ") }}{% endif %}</td>
|
||||
<td>{% if table.row_display_columns.is_empty() %}<span class="hint">id</span>{% else %}{{ table.row_display_columns|join(", ") }}{% endif %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p class="hint">Nothing is selected yet. Every panel below acts on one profile, and most of them on one table inside it.</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
{% if let Some(detail) = page.detail %}
|
||||
<section class="panel">
|
||||
<h2>{{ detail.name }} <span class="count">{{ detail.columns.len() }} columns</span></h2>
|
||||
<p class="hint">
|
||||
Identified by
|
||||
{%- if detail.row_display_columns.is_empty() %} its id
|
||||
{%- else %} {{ detail.row_display_columns|join(", ") }}{% endif -%}
|
||||
{%- if let Some(summary) = page.selected_table() -%}
|
||||
{%- if !summary.depends_on.is_empty() %} · depends on {{ summary.depends_on|join(", ") }}{% endif -%}
|
||||
{%- endif -%}
|
||||
.
|
||||
</p>
|
||||
|
||||
{% if detail.is_system() %}
|
||||
<p class="hint">This table is backend-managed. Its definition is shown here, but it can only be changed through the backend's own APIs.</p>
|
||||
{% endif %}
|
||||
|
||||
<table class="builder-table">
|
||||
<thead><tr><th>Column</th><th>Type</th><th>Notes</th></tr></thead>
|
||||
<tbody>
|
||||
{% for column in detail.columns %}
|
||||
<tr>
|
||||
<td><code>{{ column.name }}</code></td>
|
||||
<td>{{ column.field_type }}</td>
|
||||
<td>
|
||||
{%- for flag in column.flags() %}<span class="tag">{{ flag }}</span>{% endfor -%}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% if !detail.scripts.is_empty() %}
|
||||
<h3 class="panel-subhead">Scripts</h3>
|
||||
<table class="builder-table">
|
||||
<thead><tr><th>Target column</th><th>Type</th><th>Description</th></tr></thead>
|
||||
<tbody>
|
||||
{% for script in detail.scripts %}
|
||||
<tr>
|
||||
<td><code>{{ script.target_column }}</code></td>
|
||||
<td>{{ script.target_column_type }}</td>
|
||||
<td>
|
||||
{{ script.description }}
|
||||
<details><summary>source</summary><pre class="sql-preview">{{ script.script }}</pre></details>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
{% if page.table_is_writable() %}
|
||||
{#
|
||||
Append columns. The panel stages columns without writing anything; the
|
||||
selection travels in the URL so the posted fields are exactly the ones the
|
||||
Add-table builder posts, and both are decoded by the same code.
|
||||
#}
|
||||
<section class="panel">
|
||||
<h2>Add columns to <code>{{ page.selection.table }}</code></h2>
|
||||
<p class="hint">Columns are appended. Nothing that already exists is changed, and the new columns can be indexed as they are added.</p>
|
||||
<form id="column-form"
|
||||
hx-post="/admin/table-definition/columns{{ page.selection.query() }}"
|
||||
hx-target="#table-definition-workspace" hx-swap="innerHTML"
|
||||
hx-disabled-elt="button[type=submit]">
|
||||
<div id="column-panel">
|
||||
{% include "pages/admin/table_definition/column_panel.html" %}
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
{% if let Some(detail) = page.detail %}
|
||||
<section class="panel">
|
||||
<h2>Rename a column</h2>
|
||||
<p class="hint">Renames what the column is called, not the physical column underneath, so stored data and scripts are untouched. Only possible while the table has no rows.</p>
|
||||
<form hx-post="/admin/table-definition/rename"
|
||||
hx-target="#table-definition-workspace" hx-swap="innerHTML">
|
||||
<input type="hidden" name="profile" value="{{ page.selection.profile }}">
|
||||
<input type="hidden" name="table" value="{{ page.selection.table }}">
|
||||
<div class="form-grid">
|
||||
<label>Column
|
||||
<select name="old_column_name">
|
||||
<option value="">Choose a column</option>
|
||||
{% for column in detail.renameable_columns() %}
|
||||
<option value="{{ column.name }}" {% if page.rename.old_column_name == column.name %}selected{% endif %}>{{ column.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<label>New name
|
||||
<input name="new_column_name" value="{{ page.rename.new_column_name }}" placeholder="invoice_number">
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="submit">Rename column</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
<section class="panel danger-panel">
|
||||
<h2>Delete <code>{{ page.selection.table }}</code></h2>
|
||||
<p class="hint">
|
||||
Drops the table and its definition, and the profile too when this was its
|
||||
last table. The backend refuses to delete a table that still has rows.
|
||||
</p>
|
||||
<form hx-post="/admin/table-definition/delete"
|
||||
hx-target="#table-definition-workspace" hx-swap="innerHTML">
|
||||
<input type="hidden" name="profile" value="{{ page.selection.profile }}">
|
||||
<input type="hidden" name="table" value="{{ page.selection.table }}">
|
||||
<div class="form-grid">
|
||||
<label class="wide">Type <code>{{ page.selection.table }}</code> to confirm
|
||||
<input name="confirm_table_name" value="" autocomplete="off" placeholder="{{ page.selection.table }}">
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="danger-submit">Delete table</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
{% if page.selection.has_profile() %}
|
||||
<section class="panel">
|
||||
<h2>Copy <code>{{ page.selection.profile }}</code> into a new profile</h2>
|
||||
<p class="hint">Copies structure — tables, links and scripts — and no rows. Leave every table unticked to copy the whole profile.</p>
|
||||
<form hx-post="/admin/table-definition/copy"
|
||||
hx-target="#table-definition-workspace" hx-swap="innerHTML">
|
||||
<input type="hidden" name="profile" value="{{ page.selection.profile }}">
|
||||
<div class="form-grid">
|
||||
<label>New profile name
|
||||
<input name="target_profile_name" value="{{ page.copy.target_profile_name }}" placeholder="billing_2027">
|
||||
</label>
|
||||
</div>
|
||||
{% if !page.copy_candidates().is_empty() %}
|
||||
<div class="check-group">
|
||||
{% for table in page.copy_candidates() %}
|
||||
<label class="check">
|
||||
<input type="checkbox" name="table_names" value="{{ table.name }}"
|
||||
{% if page.copy_selected(table.name.as_str()) %}checked{% endif %}>
|
||||
{{ table.name }}
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="form-actions">
|
||||
<button type="submit">Copy profile</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<h2>Create tables from an invoice template</h2>
|
||||
<p class="hint">
|
||||
Reads the <code>#let komp_ac_fields = (…)</code> contract out of a Typst
|
||||
template. A path that matches an existing table and column becomes a
|
||||
reference to it; anything else becomes a TEXT column to refine later, and
|
||||
every <code>[]</code> becomes a child table.
|
||||
</p>
|
||||
<form hx-post="/admin/table-definition/invoice-template"
|
||||
hx-target="#table-definition-workspace" hx-swap="innerHTML"
|
||||
hx-disabled-elt="button[type=submit]">
|
||||
<input type="hidden" name="profile" value="{{ page.selection.profile }}">
|
||||
<div class="form-grid">
|
||||
<label>Table name
|
||||
<input name="table_name" value="{{ page.invoice.table_name }}" placeholder="invoice">
|
||||
</label>
|
||||
<label>Row display columns
|
||||
<input name="row_display_columns" value="{{ page.invoice.row_display_columns }}" placeholder="number, issued_on">
|
||||
<small>Comma-separated, and only for the root table.</small>
|
||||
</label>
|
||||
<label class="wide">Template source
|
||||
<textarea name="typst_source" rows="10" placeholder="#let komp_ac_fields = ( "sidlo.nazov", "people[].name", )">{{ page.invoice.typst_source }}</textarea>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="submit">Create from template</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<h2>Column rename history</h2>
|
||||
{% if page.history.is_empty() %}
|
||||
<p class="hint">
|
||||
No column in {% if page.selection.has_table() %}<code>{{ page.selection.table }}</code>{% else %}this profile{% endif %} has been renamed.
|
||||
</p>
|
||||
{% else %}
|
||||
<table class="builder-table">
|
||||
<thead><tr><th>Table</th><th>Was</th><th>Is</th><th>When</th></tr></thead>
|
||||
<tbody>
|
||||
{% for entry in page.history %}
|
||||
<tr>
|
||||
<td><code>{{ entry.table_name }}</code></td>
|
||||
<td><code>{{ entry.old_column_name }}</code></td>
|
||||
<td><code>{{ entry.new_column_name }}</code></td>
|
||||
<td>{{ entry.created_at }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endif %}
|
||||
Reference in New Issue
Block a user