internally hidden column forgotten

This commit is contained in:
Priec
2026-08-12 17:04:52 +02:00
parent 771109f61c
commit fa50f29ff0
8 changed files with 159 additions and 38 deletions

View File

@@ -3,7 +3,11 @@ use axum::http::HeaderMap;
use crate::{
AppState,
auth::GetAuthorizationRequest,
definitions::{common::Empty, table_structure::GetTableStructureRequest},
definitions::{
common::Empty, table_definition::GetTableCatalogRequest,
table_structure::GetTableStructureRequest,
},
pages::GLOBAL_SCOPE,
services::{AuthenticationError, authenticated_request},
};
@@ -40,32 +44,52 @@ pub(crate) async fn load_admin_page(
.map_err(|error| LoadError::Backend(error.message().to_string()))?
.into_inner();
let profiles = profile_tree
.profiles
.iter()
.map(|profile| ProfileView {
name: profile.name.clone(),
table_count: profile.tables.len(),
})
.collect::<Vec<_>>();
// Global tables belong to every profile, so the tree repeats them under
// each one. They are browsed under the global scope instead, and a
// profile shows only the tables it owns.
let global_tables = definitions
.get_table_catalog(
authenticated_request(headers, GetTableCatalogRequest { profile_name: None })
.map_err(authentication_error)?,
)
.await
.map_err(|error| LoadError::Backend(error.message().to_string()))?
.into_inner()
.tables;
let mut profiles = vec![ProfileView {
label: "Global".to_string(),
scope: GLOBAL_SCOPE.to_string(),
table_count: global_tables.len(),
global: true,
}];
profiles.extend(profile_tree.profiles.iter().map(|profile| ProfileView {
label: profile.name.clone(),
scope: profile.name.clone(),
table_count: profile.tables.iter().filter(|table| !table.global).count(),
global: false,
}));
let selected_profile = (!selection.profile.is_empty()).then_some(selection.profile);
let profile = match selected_profile.as_deref() {
let selected_tables = match selected_profile.as_deref() {
Some(GLOBAL_SCOPE) => Some(global_tables.as_slice()),
Some(name) => Some(
profile_tree
.profiles
.iter()
.find(|profile| profile.name == name)
.ok_or_else(|| LoadError::InvalidSelection(format!("Unknown profile '{name}'")))?,
.ok_or_else(|| LoadError::InvalidSelection(format!("Unknown profile '{name}'")))?
.tables
.as_slice(),
),
None => None,
};
let tables = profile
.map(|profile| {
profile
.tables
let tables = selected_tables
.map(|tables| {
tables
.iter()
.filter(|table| table.global == (selected_profile.as_deref() == Some(GLOBAL_SCOPE)))
.map(|table| TableView {
name: table.name.clone(),
// One entry per link, named by the column carrying it, so a
@@ -85,7 +109,7 @@ pub(crate) async fn load_admin_page(
let selected_table = (!selection.table.is_empty()).then_some(selection.table);
if let Some(table_name) = selected_table.as_deref() {
if profile.is_none() || !tables.iter().any(|table| table.name == table_name) {
if selected_tables.is_none() || !tables.iter().any(|table| table.name == table_name) {
return Err(LoadError::InvalidSelection(format!(
"Table '{table_name}' is not part of the selected profile"
)));

View File

@@ -21,6 +21,11 @@ pub(crate) struct AdminPageState {
}
impl AdminPageState {
/// Whether the pane is pointed at the global scope rather than a profile.
pub(crate) fn is_global(&self) -> bool {
self.selected_profile.as_deref() == Some(crate::pages::GLOBAL_SCOPE)
}
/// 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> {
@@ -34,10 +39,16 @@ impl AdminPageState {
}
}
/// One entry in the Profiles pane: a real profile, or the global scope the
/// shared tables live in.
#[derive(Debug)]
pub(crate) struct ProfileView {
pub name: String,
/// What the pane shows.
pub label: String,
/// What the pane posts as `?profile=`.
pub scope: String,
pub table_count: usize,
pub global: bool,
}
#[derive(Debug)]

View File

@@ -81,6 +81,45 @@ mod tests {
}
}
#[test]
fn the_profiles_pane_offers_the_global_scope() {
use crate::pages::admin::admin::state::ProfileView;
let page = AdminPageState {
nav: Nav::default(),
profiles: vec![
ProfileView {
label: "Global".to_string(),
scope: crate::pages::GLOBAL_SCOPE.to_string(),
table_count: 2,
global: true,
},
ProfileView {
label: "books".to_string(),
scope: "books".to_string(),
table_count: 5,
global: false,
},
],
selected_profile: Some(crate::pages::GLOBAL_SCOPE.to_string()),
tables: Vec::new(),
selected_table: None,
columns: Vec::new(),
can_manage_tables: true,
can_manage_scripts: true,
can_manage_validations: true,
can_export: true,
};
let html = render_workspace(&page);
assert!(html.contains(r#"name="profile" value="__global""#));
assert!(html.contains("shared by every profile"));
// Selecting it marks it, and its empty state names the global scope
// rather than a profile that has no tables.
assert!(html.contains("browser-item selected"));
assert!(html.contains("There are no global tables."));
}
#[test]
fn system_columns_render_after_the_user_defined_ones() {
let column = |name: &str, system: bool| crate::pages::admin::admin::state::ColumnView {

View File

@@ -7,7 +7,7 @@
use crate::schema::{ColumnCatalog, ColumnDraft};
pub(crate) const GLOBAL_SCOPE: &str = "__global";
pub(crate) use crate::pages::GLOBAL_SCOPE;
/// 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

View File

@@ -1,3 +1,10 @@
/// The pseudo-profile a page posts when the selection is the global scope.
///
/// Global tables belong to every profile, so the backend keeps them in a
/// schema of their own rather than in any one profile's. A selector that lists
/// profiles offers this alongside them, and the backend recognises the name.
pub(crate) const GLOBAL_SCOPE: &str = "__global";
pub(crate) mod add_logic;
pub(crate) mod add_table;
pub(crate) mod add_validation;