334 lines
14 KiB
HTML
334 lines
14 KiB
HTML
{#
|
|
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 %}
|
|
{% import "ui/toast.html" as toast %}
|
|
|
|
{%- if let Some(message) = page.error %}{% call alert::error("Could not continue", message) %}{% endcall %}{% endif -%}
|
|
{%- if let Some(message) = page.status %}{% call toast::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 }}{% if !column.sql_type.is_empty() %} <small class="hint">{{ column.sql_type }}</small>{% endif %}</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.permission_object.is_empty() %}
|
|
<section class="panel">
|
|
<h2>Data permissions for <code>{{ page.selection.table }}</code></h2>
|
|
<p class="hint">These grants cover this table family. A successful change retires the current session, so you will be asked to sign in again.</p>
|
|
<table class="builder-table">
|
|
<thead><tr><th>Role</th><th>Actions</th></tr></thead>
|
|
<tbody>{% for role in page.role_permissions %}<tr>
|
|
<td>{{ role.role }}</td>
|
|
<td><div class="actions">{% for permission in role.actions %}
|
|
{% if permission.direct %}
|
|
<form hx-post="/admin/permissions/revoke" hx-target="#permission-status">
|
|
<input type="hidden" name="role" value="{{ role.role }}"><input type="hidden" name="object" value="{{ page.permission_object }}"><input type="hidden" name="action" value="{{ permission.action }}">
|
|
<button type="submit" class="danger">{{ permission.action }} ✓</button>
|
|
</form>
|
|
{% else if permission.effective %}
|
|
<span class="tag">{{ permission.action }} · inherited</span>
|
|
{% else %}
|
|
<form hx-post="/admin/permissions/grant" hx-target="#permission-status">
|
|
<input type="hidden" name="role" value="{{ role.role }}"><input type="hidden" name="object" value="{{ page.permission_object }}"><input type="hidden" name="action" value="{{ permission.action }}">
|
|
<button type="submit" class="secondary">Grant {{ permission.action }}</button>
|
|
</form>
|
|
{% endif %}
|
|
{% endfor %}</div></td>
|
|
</tr>{% endfor %}</tbody>
|
|
</table>
|
|
<div id="permission-status" aria-live="polite"></div>
|
|
</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 %}
|