web displays system or user defined columns

This commit is contained in:
Priec
2026-08-12 16:48:56 +02:00
parent bd76efeaac
commit 771109f61c
8 changed files with 85 additions and 4 deletions

2
client

Submodule client updated: 819a4b6300...e17321c951

2
server

Submodule server updated: 32b38e5a5b...8b7a88ec3e

View File

@@ -12,6 +12,12 @@ mod schema;
mod services; mod services;
mod ui; mod ui;
mod authz; mod authz;
// The server's system column vocabulary, read out of `common` the same way the
// generated protos are: this crate compiles that source tree directly instead
// of depending on the crate. Only `is_system_column` is used here.
#[path = "../../common/src/system_column.rs"]
#[allow(dead_code)]
mod system_column;
mod analytics { mod analytics {
include!(concat!( include!(concat!(
env!("CARGO_MANIFEST_DIR"), env!("CARGO_MANIFEST_DIR"),

View File

@@ -122,6 +122,7 @@ pub(crate) async fn load_admin_page(
nullable: column.is_nullable, nullable: column.is_nullable,
primary_key: column.is_primary_key, primary_key: column.is_primary_key,
quantity_ledger: column.quantity_ledger, quantity_ledger: column.quantity_ledger,
system: crate::system_column::is_system_column(&column.name),
}) })
.collect() .collect()
} }

View File

@@ -20,6 +20,20 @@ pub(crate) struct AdminPageState {
pub can_export: bool, pub can_export: bool,
} }
impl AdminPageState {
/// The columns the user defined, in the order the backend reported them.
/// The columns pane renders these first.
pub(crate) fn user_columns(&self) -> Vec<&ColumnView> {
self.columns.iter().filter(|column| !column.system).collect()
}
/// The columns the server puts on every managed table. They are hidden
/// everywhere else, so they trail the user's own here.
pub(crate) fn system_columns(&self) -> Vec<&ColumnView> {
self.columns.iter().filter(|column| column.system).collect()
}
}
#[derive(Debug)] #[derive(Debug)]
pub(crate) struct ProfileView { pub(crate) struct ProfileView {
pub name: String, pub name: String,
@@ -40,12 +54,18 @@ pub(crate) struct ColumnView {
pub nullable: bool, pub nullable: bool,
pub primary_key: bool, pub primary_key: bool,
pub quantity_ledger: bool, pub quantity_ledger: bool,
/// The server named this column, not the user. Ordinary screens never show
/// it, so the admin panel is the only place it is visible.
pub system: bool,
} }
impl ColumnView { impl ColumnView {
/// The badge list rendered under each column name. /// The badge list rendered under each column name.
pub(crate) fn flags(&self) -> Vec<&'static str> { pub(crate) fn flags(&self) -> Vec<&'static str> {
let mut flags = Vec::new(); let mut flags = Vec::new();
if self.system {
flags.push("system");
}
if self.primary_key { if self.primary_key {
flags.push("primary key"); flags.push("primary key");
} }

View File

@@ -80,4 +80,41 @@ mod tests {
assert!(html.contains(route), "missing admin action route {route}"); assert!(html.contains(route), "missing admin action route {route}");
} }
} }
#[test]
fn system_columns_render_after_the_user_defined_ones() {
let column = |name: &str, system: bool| crate::pages::admin::admin::state::ColumnView {
name: name.to_string(),
data_type: "TEXT".to_string(),
nullable: true,
primary_key: false,
quantity_ledger: false,
system,
};
let page = AdminPageState {
nav: Nav::default(),
profiles: Vec::new(),
selected_profile: Some("books".to_string()),
tables: Vec::new(),
selected_table: Some("invoice".to_string()),
// The backend reports the physical order, system columns first.
columns: vec![
column("id", true),
column("title", false),
column("created_at", true),
],
can_manage_tables: true,
can_manage_scripts: true,
can_manage_validations: true,
can_export: true,
};
let html = render_workspace(&page);
let title = html.find(">title<").expect("the user column is rendered");
let label = html
.find("System columns")
.expect("the system group is labelled");
let id = html.find(">id<").expect("the system column is rendered");
assert!(title < label && label < id, "system columns must trail");
}
} }

View File

@@ -55,6 +55,9 @@
.column { display: grid; grid-template-columns: minmax(100px, 1fr) auto; gap: 3px 12px; padding: 10px 11px; border-bottom: 1px solid #edf0f3; } .column { display: grid; grid-template-columns: minmax(100px, 1fr) auto; gap: 3px 12px; padding: 10px 11px; border-bottom: 1px solid #edf0f3; }
.column code { color: #315078; font-size: 12px; } .column code { color: #315078; font-size: 12px; }
.column small { grid-column: 1 / -1; } .column small { grid-column: 1 / -1; }
.column.system { color: #6b7686; background: #fafbfc; }
.column.system code { color: #67788e; }
.group-label { margin: 14px 0 4px; padding: 0 11px; color: #7c8796; font-size: 11px; letter-spacing: .04em; text-transform: uppercase; }
.empty { margin: 8px; color: #7c8796; } .empty { margin: 8px; color: #7c8796; }
/* ---------- Forms (components/form_card.html) ---------- */ /* ---------- Forms (components/form_card.html) ---------- */

View File

@@ -49,20 +49,34 @@
</section> </section>
<section class="pane"> <section class="pane">
<div class="pane-title"><h2>Columns</h2><span>{{ page.columns.len() }}</span></div> <div class="pane-title">
<h2>Columns</h2>
<span>{{ page.user_columns().len() }} user · {{ page.system_columns().len() }} system</span>
</div>
<div class="pane-list"> <div class="pane-list">
{% if page.selected_table.is_none() %} {% if page.selected_table.is_none() %}
<p class="empty">Select a table to inspect its columns.</p> <p class="empty">Select a table to inspect its columns.</p>
{% else if page.columns.is_empty() %} {% else if page.columns.is_empty() %}
<p class="empty">This table has no visible columns.</p> <p class="empty">This table has no visible columns.</p>
{% else %} {% else %}
{% for column in page.columns %} {% for column in page.user_columns() %}
<div class="column"> <div class="column">
<span>{{ column.name }}</span> <span>{{ column.name }}</span>
<code>{{ column.data_type }}</code> <code>{{ column.data_type }}</code>
<small>{{ column.flags()|join(" · ") }}</small> <small>{{ column.flags()|join(" · ") }}</small>
</div> </div>
{% endfor %} {% endfor %}
{% let system_columns = page.system_columns() %}
{% if !system_columns.is_empty() %}
<p class="group-label">System columns · hidden from users</p>
{% for column in system_columns %}
<div class="column system">
<span>{{ column.name }}</span>
<code>{{ column.data_type }}</code>
<small>{{ column.flags()|join(" · ") }}</small>
</div>
{% endfor %}
{% endif %}
{% endif %} {% endif %}
</div> </div>
</section> </section>