global table content

This commit is contained in:
Priec
2026-08-12 14:10:20 +02:00
parent 112bea6519
commit 78e3f1ae08
7 changed files with 54 additions and 17 deletions

2
server

Submodule server updated: d8c8b3521c...2bfe955a8d

View File

@@ -29,6 +29,8 @@ use super::{
pub(crate) struct NewTableQuery { pub(crate) struct NewTableQuery {
#[serde(default)] #[serde(default)]
profile: String, profile: String,
#[serde(default)]
global: bool,
} }
/// GET /admin/tables/new /// GET /admin/tables/new
@@ -39,6 +41,7 @@ pub(crate) async fn new_table_page(
) -> Response { ) -> Response {
let mut draft = TableDraft::new(); let mut draft = TableDraft::new();
draft.profile_name = query.profile.trim().to_string(); draft.profile_name = query.profile.trim().to_string();
draft.global = query.global;
match load_page(state, &headers, draft, None, None).await { match load_page(state, &headers, draft, None, None).await {
Ok(page) => Html(ui::render_page(&page)).into_response(), Ok(page) => Html(ui::render_page(&page)).into_response(),
@@ -91,7 +94,11 @@ pub(crate) async fn create_table(
} }
}; };
let profile_name = request.profile_name.clone(); let profile_name = if request.global {
"__global".to_string()
} else {
request.profile_name.clone()
};
let request = match authenticated_request(&headers, request) { let request = match authenticated_request(&headers, request) {
Ok(request) => request, Ok(request) => request,
Err(_) => return Redirect::to("/login").into_response(), Err(_) => return Redirect::to("/login").into_response(),

View File

@@ -26,7 +26,7 @@ use crate::{
}; };
use super::state::{ use super::state::{
DetailColumn, LoadError, PageInputs, RenameEntry, ScriptView, TableDefinitionPageState, DetailColumn, GLOBAL_SCOPE, LoadError, PageInputs, RenameEntry, ScriptView, TableDefinitionPageState,
TableDetailView, TablePermissionAction, TableRolePermissions, TableSummary, TableDetailView, TablePermissionAction, TableRolePermissions, TableSummary,
}; };
@@ -96,19 +96,24 @@ pub(crate) async fn load_page(
.collect::<Vec<_>>(); .collect::<Vec<_>>();
// A profile that no longer exists takes the table selection with it. // A profile that no longer exists takes the table selection with it.
if !profiles.contains(&inputs.selection.profile) { if inputs.selection.profile != GLOBAL_SCOPE && !profiles.contains(&inputs.selection.profile) {
inputs.selection.profile.clear(); inputs.selection.profile.clear();
inputs.selection.table.clear(); inputs.selection.table.clear();
} }
let tables = tree let selected_tables = if inputs.selection.is_global() {
.profiles tree.profiles.first().map(|profile| profile.tables.as_slice())
.iter() } else {
.find(|profile| profile.name == inputs.selection.profile) tree.profiles
.map(|profile| { .iter()
profile .find(|profile| profile.name == inputs.selection.profile)
.tables .map(|profile| profile.tables.as_slice())
};
let tables = selected_tables
.map(|tables| {
tables
.iter() .iter()
.filter(|table| table.global == inputs.selection.is_global())
.map(|table| TableSummary { .map(|table| TableSummary {
name: table.name.clone(), name: table.name.clone(),
table_kind: table.table_kind.clone(), table_kind: table.table_kind.clone(),

View File

@@ -7,6 +7,8 @@
use crate::schema::{ColumnCatalog, ColumnDraft}; use crate::schema::{ColumnCatalog, ColumnDraft};
pub(crate) const GLOBAL_SCOPE: &str = "__global";
/// The profile and table the workspace is pointed at. Arrives as a query /// The profile and table the workspace is pointed at. Arrives as a query
/// string on the selector and on the column-panel endpoints, and as hidden /// string on the selector and on the column-panel endpoints, and as hidden
/// fields on the panels that write. /// fields on the panels that write.
@@ -27,6 +29,10 @@ impl Selection {
!self.table.is_empty() !self.table.is_empty()
} }
pub(crate) fn is_global(&self) -> bool {
self.profile == GLOBAL_SCOPE
}
/// The query string the column panel posts back to, so the panel's own /// The query string the column panel posts back to, so the panel's own
/// form does not have to carry the selection among its column fields. /// form does not have to carry the selection among its column fields.
pub(crate) fn query(&self) -> String { pub(crate) fn query(&self) -> String {

View File

@@ -113,6 +113,20 @@ mod tests {
} }
} }
#[test]
fn global_tables_have_their_own_scope() {
let mut state = page();
state.selection.profile = "__global".to_string();
let html = render_workspace(&state);
assert!(html.contains(r#"value="__global" selected"#));
assert!(html.contains("Global — all profiles"));
assert!(html.contains("/admin/tables/new?global=true"));
assert!(!html.contains("Copy <code>__global</code>"));
assert!(!html.contains("Create tables from an invoice template"));
}
fn page() -> TableDefinitionPageState { fn page() -> TableDefinitionPageState {
TableDefinitionPageState { TableDefinitionPageState {
nav: Nav::default(), nav: Nav::default(),
@@ -254,7 +268,7 @@ mod tests {
state.tables.clear(); state.tables.clear();
let html = render_workspace(&state); let html = render_workspace(&state);
assert!(!html.contains("/admin/table-definition/copy")); assert!(!html.contains("/admin/table-definition/copy"));
assert!(html.contains("Choose a profile")); assert!(html.contains("Choose a scope"));
} }
#[test] #[test]

View File

@@ -9,7 +9,7 @@
<div> <div>
<p class="eyebrow">Table definition</p> <p class="eyebrow">Table definition</p>
<h1>Table definition</h1> <h1>Table definition</h1>
<p>Pick a profile and a table, then change its definition.</p> <p>Choose Global to manage tables shared by every profile, or choose a profile to manage only its own tables.</p>
</div> </div>
<div class="actions"> <div class="actions">
<a href="/admin">← Admin panel</a> <a href="/admin">← Admin panel</a>

View File

@@ -45,10 +45,11 @@
<section class="panel"> <section class="panel">
<h2>Selection</h2> <h2>Selection</h2>
<div class="form-grid"> <div class="form-grid">
<label>Profile <label>Scope
<select name="profile" hx-get="/admin/table-definition/workspace" <select name="profile" hx-get="/admin/table-definition/workspace"
hx-target="#table-definition-workspace" hx-swap="innerHTML"> hx-target="#table-definition-workspace" hx-swap="innerHTML">
<option value="">Choose a profile</option> <option value="">Choose a scope</option>
<option value="__global" {% if page.selection.is_global() %}selected{% endif %}>Global — all profiles</option>
{% for profile in page.profiles %} {% for profile in page.profiles %}
<option value="{{ profile }}" {% if page.selection.profile == *profile %}selected{% endif %}>{{ profile }}</option> <option value="{{ profile }}" {% if page.selection.profile == *profile %}selected{% endif %}>{{ profile }}</option>
{% endfor %} {% endfor %}
@@ -73,7 +74,11 @@
{% if page.selection.has_profile() %} {% if page.selection.has_profile() %}
<div class="actions panel-actions"> <div class="actions panel-actions">
<a href="/admin/tables/new?profile={{ page.selection.profile }}">+ Create a table in this profile</a> {% if page.selection.is_global() %}
<a href="/admin/tables/new?global=true">+ Create a global table</a>
{% else %}
<a href="/admin/tables/new?profile={{ page.selection.profile }}">+ Create a table in this profile</a>
{% endif %}
</div> </div>
{% if page.tables.is_empty() %} {% if page.tables.is_empty() %}
@@ -253,7 +258,7 @@
</section> </section>
{% endif %} {% endif %}
{% if page.selection.has_profile() %} {% if page.selection.has_profile() && !page.selection.is_global() %}
<section class="panel"> <section class="panel">
<h2>Copy <code>{{ page.selection.profile }}</code> into a new profile</h2> <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> <p class="hint">Copies structure — tables, links and scripts — and no rows. Leave every table unticked to copy the whole profile.</p>