global table profile name from server in web now

This commit is contained in:
Priec
2026-08-17 13:24:34 +02:00
parent efd9e2481b
commit f0d12584ef
15 changed files with 128 additions and 57 deletions

View File

@@ -12,6 +12,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
--- ---
## [Unreleased]
### Changed
- **The shared profile is named by the backend** — `ProfileTreeResponse`
gained `shared_profile_name`, and every page that browses the shared tables
now reads the name from there instead of spelling `__global` itself. The
admin panel, the table-definition workspace, the add-table builder and the
transfer pages each pass that name as `GetTableCatalogRequest.profile_name`,
which is no longer optional: the shared profile is asked for by name like
any other.
---
## [v0.8.46] — 2026-08-16 ## [v0.8.46] — 2026-08-16
### Added ### Added

View File

@@ -70,11 +70,13 @@ pub(crate) async fn load_page(
// The global scope is the catalog's answer, here as everywhere else, so a // The global scope is the catalog's answer, here as everywhere else, so a
// shared table the admin panel lists is a shared table this page can link // shared table the admin panel lists is a shared table this page can link
// to — see `crate::pages::table_scope`. // to — see `crate::pages::table_scope`. Which profile holds the shared
// tables is the server's to say, and the tree above says it.
let shared_profile = tree.shared_profile_name.clone();
let catalog_tables = definitions let catalog_tables = definitions
.get_table_catalog( .get_table_catalog(
authenticated_request(headers, GetTableCatalogRequest { authenticated_request(headers, GetTableCatalogRequest {
profile_name: crate::pages::GLOBAL_SCOPE.to_string(), profile_name: shared_profile.clone(),
}) })
.map_err(|_| LoadError::Unauthenticated)?, .map_err(|_| LoadError::Unauthenticated)?,
) )
@@ -127,6 +129,7 @@ pub(crate) async fn load_page(
Ok(AddTablePageState { Ok(AddTablePageState {
nav: crate::ui::Nav::from_authorization(headers, "admin", &authorization), nav: crate::ui::Nav::from_authorization(headers, "admin", &authorization),
shared_profile,
profiles: tree profiles: tree
.profiles .profiles
.into_iter() .into_iter()

View File

@@ -95,8 +95,10 @@ pub(crate) async fn create_table(
} }
}; };
// A global table is stored in the shared profile, which the loader read
// off the profile tree; the redirect below browses it under that name.
let profile_name = if request.global { let profile_name = if request.global {
"__global".to_string() page.shared_profile.clone()
} else { } else {
request.profile_name.clone() request.profile_name.clone()
}; };
@@ -245,6 +247,7 @@ mod tests {
}); });
AddTablePageState { AddTablePageState {
nav: crate::ui::Nav::default(), nav: crate::ui::Nav::default(),
shared_profile: "__global".to_string(),
profiles: vec!["billing".to_string()], profiles: vec!["billing".to_string()],
draft, draft,
status: None, status: None,

View File

@@ -192,6 +192,9 @@ impl BuilderForm {
pub(crate) struct AddTablePageState { pub(crate) struct AddTablePageState {
pub nav: crate::ui::Nav, pub nav: crate::ui::Nav,
pub profiles: Vec<String>, pub profiles: Vec<String>,
/// The profile a global table lands in, as the profile tree reports it.
/// The builder posts it as the created table's scope.
pub shared_profile: String,
pub draft: TableDraft, pub draft: TableDraft,
/// The outcome of the last builder action, if any. /// The outcome of the last builder action, if any.
pub status: Option<String>, pub status: Option<String>,
@@ -492,6 +495,7 @@ mod tests {
fn the_column_list_shows_what_a_definition_row_expands_into() { fn the_column_list_shows_what_a_definition_row_expands_into() {
let mut page = AddTablePageState { let mut page = AddTablePageState {
nav: crate::ui::Nav::default(), nav: crate::ui::Nav::default(),
shared_profile: "__global".to_string(),
profiles: Vec::new(), profiles: Vec::new(),
draft: posted_form().to_draft(), draft: posted_form().to_draft(),
status: None, status: None,
@@ -567,6 +571,7 @@ mod tests {
let page = AddTablePageState { let page = AddTablePageState {
nav: crate::ui::Nav::default(), nav: crate::ui::Nav::default(),
shared_profile: "__global".to_string(),
profiles: Vec::new(), profiles: Vec::new(),
draft, draft,
status: None, status: None,

View File

@@ -109,6 +109,7 @@ mod tests {
AddTablePageState { AddTablePageState {
nav: Nav::default(), nav: Nav::default(),
shared_profile: "__global".to_string(),
profiles: vec!["billing".to_string()], profiles: vec!["billing".to_string()],
draft, draft,
status: None, status: None,

View File

@@ -7,7 +7,7 @@ use crate::{
common::Empty, table_definition::GetTableCatalogRequest, common::Empty, table_definition::GetTableCatalogRequest,
table_structure::GetTableStructureRequest, table_structure::GetTableStructureRequest,
}, },
pages::{GLOBAL_SCOPE, table_scope}, pages::table_scope,
services::{AuthenticationError, authenticated_request}, services::{AuthenticationError, authenticated_request},
}; };
@@ -45,14 +45,16 @@ pub(crate) async fn load_admin_page(
.into_inner(); .into_inner();
// Global tables belong to every profile, so the tree repeats them under // Global tables belong to every profile, so the tree repeats them under
// each one. They are browsed under the global scope instead, and a // each one. They are browsed under the shared profile instead, whose name
// profile shows only the tables it owns. The catalog is where every page // only the server knows, and a profile shows only the tables it owns. The
// reads the global scope from — see `crate::pages::table_scope`. // catalog is where every page reads the global scope from — see
// `crate::pages::table_scope`.
let shared_profile = profile_tree.shared_profile_name.clone();
let global_tables = table_scope::global_tables( let global_tables = table_scope::global_tables(
&definitions &definitions
.get_table_catalog( .get_table_catalog(
authenticated_request(headers, GetTableCatalogRequest { authenticated_request(headers, GetTableCatalogRequest {
profile_name: crate::pages::GLOBAL_SCOPE.to_string(), profile_name: shared_profile.clone(),
}) })
.map_err(authentication_error)?, .map_err(authentication_error)?,
) )
@@ -67,7 +69,7 @@ pub(crate) async fn load_admin_page(
crate::i18n::Locale::from_headers(headers), crate::i18n::Locale::from_headers(headers),
"admin-global-label" "admin-global-label"
), ),
scope: GLOBAL_SCOPE.to_string(), scope: shared_profile.clone(),
table_count: global_tables.len(), table_count: global_tables.len(),
global: true, global: true,
}]; }];
@@ -80,7 +82,7 @@ pub(crate) async fn load_admin_page(
let selected_profile = (!selection.profile.is_empty()).then_some(selection.profile); let selected_profile = (!selection.profile.is_empty()).then_some(selection.profile);
let selected_tables = match selected_profile.as_deref() { let selected_tables = match selected_profile.as_deref() {
Some(GLOBAL_SCOPE) => Some(global_tables), Some(name) if name == shared_profile => Some(global_tables),
Some(name) => { Some(name) => {
if !profile_tree.profiles.iter().any(|profile| profile.name == name) { if !profile_tree.profiles.iter().any(|profile| profile.name == name) {
return Err(LoadError::InvalidSelection(crate::tr!( return Err(LoadError::InvalidSelection(crate::tr!(
@@ -170,6 +172,7 @@ pub(crate) async fn load_admin_page(
Ok(AdminPageState { Ok(AdminPageState {
nav: crate::ui::Nav::from_authorization(headers, "admin", &authorization), nav: crate::ui::Nav::from_authorization(headers, "admin", &authorization),
profiles, profiles,
shared_profile,
selected_profile, selected_profile,
tables, tables,
selected_table, selected_table,

View File

@@ -10,6 +10,9 @@ pub(crate) struct AdminSelection {
pub(crate) struct AdminPageState { pub(crate) struct AdminPageState {
pub nav: crate::ui::Nav, pub nav: crate::ui::Nav,
pub profiles: Vec<ProfileView>, pub profiles: Vec<ProfileView>,
/// The profile the shared tables are stored in, as the profile tree
/// reports it. The page never spells the name itself.
pub shared_profile: String,
pub selected_profile: Option<String>, pub selected_profile: Option<String>,
pub tables: Vec<TableView>, pub tables: Vec<TableView>,
pub selected_table: Option<String>, pub selected_table: Option<String>,
@@ -31,7 +34,7 @@ pub(crate) struct AdminPageState {
impl AdminPageState { impl AdminPageState {
/// Whether the pane is pointed at the global scope rather than a profile. /// Whether the pane is pointed at the global scope rather than a profile.
pub(crate) fn is_global(&self) -> bool { pub(crate) fn is_global(&self) -> bool {
self.selected_profile.as_deref() == Some(crate::pages::GLOBAL_SCOPE) self.selected_profile.as_deref() == Some(self.shared_profile.as_str())
} }
/// The columns the user defined, in the order the backend reported them. /// The columns the user defined, in the order the backend reported them.

View File

@@ -47,6 +47,11 @@ pub(crate) fn render_error(locale: Locale, message: &str) -> String {
mod tests { mod tests {
use super::*; use super::*;
/// What this deployment's server happens to call the shared profile. The
/// page is handed the name rather than knowing it, so the tests spell it
/// once here.
const SHARED_PROFILE: &str = "__global";
#[test] #[test]
fn dashboard_links_every_admin_action() { fn dashboard_links_every_admin_action() {
// The admin page is only reachable with a session, so the navbar // The admin page is only reachable with a session, so the navbar
@@ -65,6 +70,7 @@ mod tests {
let page = AdminPageState { let page = AdminPageState {
nav, nav,
profiles: Vec::new(), profiles: Vec::new(),
shared_profile: SHARED_PROFILE.to_string(),
selected_profile: None, selected_profile: None,
tables: Vec::new(), tables: Vec::new(),
selected_table: None, selected_table: None,
@@ -99,7 +105,7 @@ mod tests {
profiles: vec![ profiles: vec![
ProfileView { ProfileView {
label: "Global".to_string(), label: "Global".to_string(),
scope: crate::pages::GLOBAL_SCOPE.to_string(), scope: SHARED_PROFILE.to_string(),
table_count: 2, table_count: 2,
global: true, global: true,
}, },
@@ -110,7 +116,8 @@ mod tests {
global: false, global: false,
}, },
], ],
selected_profile: Some(crate::pages::GLOBAL_SCOPE.to_string()), shared_profile: SHARED_PROFILE.to_string(),
selected_profile: Some(SHARED_PROFILE.to_string()),
tables: Vec::new(), tables: Vec::new(),
selected_table: None, selected_table: None,
columns: Vec::new(), columns: Vec::new(),
@@ -141,6 +148,7 @@ mod tests {
let page = AdminPageState { let page = AdminPageState {
nav: Nav::default(), nav: Nav::default(),
profiles: Vec::new(), profiles: Vec::new(),
shared_profile: SHARED_PROFILE.to_string(),
selected_profile: Some("books".to_string()), selected_profile: Some("books".to_string()),
tables: vec![TableView { tables: vec![TableView {
name: "invoice".to_string(), name: "invoice".to_string(),
@@ -180,7 +188,8 @@ mod tests {
let page = AdminPageState { let page = AdminPageState {
nav: Nav::default(), nav: Nav::default(),
profiles: Vec::new(), profiles: Vec::new(),
selected_profile: Some(crate::pages::GLOBAL_SCOPE.to_string()), shared_profile: SHARED_PROFILE.to_string(),
selected_profile: Some(SHARED_PROFILE.to_string()),
tables: vec![TableView { tables: vec![TableView {
name: "currencies".to_string(), name: "currencies".to_string(),
depends_on: Vec::new(), depends_on: Vec::new(),
@@ -218,6 +227,7 @@ mod tests {
let page = AdminPageState { let page = AdminPageState {
nav: Nav::default(), nav: Nav::default(),
profiles: Vec::new(), profiles: Vec::new(),
shared_profile: SHARED_PROFILE.to_string(),
selected_profile: Some("books".to_string()), selected_profile: Some("books".to_string()),
tables: Vec::new(), tables: Vec::new(),
selected_table: Some("invoice".to_string()), selected_table: Some("invoice".to_string()),

View File

@@ -30,7 +30,7 @@ use crate::{
}; };
use super::state::{ use super::state::{
DetailColumn, GLOBAL_SCOPE, LoadError, PageInputs, RenameEntry, ScriptView, DetailColumn, LoadError, PageInputs, RenameEntry, ScriptView,
TableDefinitionPageState, TableDetailView, TableSummary, TableDefinitionPageState, TableDetailView, TableSummary,
}; };
@@ -93,6 +93,9 @@ pub(crate) async fn load_page(
.map_err(|error| LoadError::Backend(error.message().to_string()))? .map_err(|error| LoadError::Backend(error.message().to_string()))?
.into_inner(); .into_inner();
// The profile the shared tables are stored in is the server's to name, so
// it is read off the tree rather than spelled here.
let shared_profile = tree.shared_profile_name.clone();
let profiles = tree let profiles = tree
.profiles .profiles
.iter() .iter()
@@ -103,7 +106,7 @@ pub(crate) async fn load_page(
// says so — the commonest way to get here with a stale one is having just // says so — the commonest way to get here with a stale one is having just
// deleted its last table, which is worth being told. // deleted its last table, which is worth being told.
if inputs.selection.has_profile() if inputs.selection.has_profile()
&& inputs.selection.profile != GLOBAL_SCOPE && !inputs.selection.is_shared_profile(&shared_profile)
&& !profiles.contains(&inputs.selection.profile.as_str()) && !profiles.contains(&inputs.selection.profile.as_str())
{ {
let missing = std::mem::take(&mut inputs.selection.profile); let missing = std::mem::take(&mut inputs.selection.profile);
@@ -122,7 +125,7 @@ pub(crate) async fn load_page(
let catalog_tables = definitions let catalog_tables = definitions
.get_table_catalog( .get_table_catalog(
authenticated_request(headers, GetTableCatalogRequest { authenticated_request(headers, GetTableCatalogRequest {
profile_name: crate::pages::GLOBAL_SCOPE.to_string(), profile_name: shared_profile.clone(),
}) })
.map_err(|_| LoadError::Unauthenticated)?, .map_err(|_| LoadError::Unauthenticated)?,
) )
@@ -130,7 +133,7 @@ pub(crate) async fn load_page(
.map_err(|error| LoadError::Backend(error.message().to_string()))? .map_err(|error| LoadError::Backend(error.message().to_string()))?
.into_inner() .into_inner()
.tables; .tables;
let selected_tables = if inputs.selection.is_global() { let selected_tables = if inputs.selection.is_shared_profile(&shared_profile) {
table_scope::global_tables(&catalog_tables) table_scope::global_tables(&catalog_tables)
} else { } else {
table_scope::profile_owned_tables(&tree.profiles, &inputs.selection.profile) table_scope::profile_owned_tables(&tree.profiles, &inputs.selection.profile)
@@ -138,7 +141,7 @@ pub(crate) async fn load_page(
// A shared table is a link target from every scope, which is why this is // A shared table is a link target from every scope, which is why this is
// not the list above: browsing a profile shows the profile's own tables, // not the list above: browsing a profile shows the profile's own tables,
// and linking from one may reach the shared ones too. // and linking from one may reach the shared ones too.
let link_targets = if inputs.selection.is_global() { let link_targets = if inputs.selection.is_shared_profile(&shared_profile) {
table_scope::global_tables(&catalog_tables) table_scope::global_tables(&catalog_tables)
} else { } else {
table_scope::linkable_tables(&tree.profiles, &catalog_tables, &inputs.selection.profile) table_scope::linkable_tables(&tree.profiles, &catalog_tables, &inputs.selection.profile)
@@ -167,7 +170,9 @@ pub(crate) async fn load_page(
.any(|table| table.name == inputs.selection.table) .any(|table| table.name == inputs.selection.table)
{ {
let missing = std::mem::take(&mut inputs.selection.table); let missing = std::mem::take(&mut inputs.selection.table);
let scope = inputs.selection.scope_label(&crate::i18n::Locale::from_headers(headers)); let scope = inputs
.selection
.scope_label(&crate::i18n::Locale::from_headers(headers), &shared_profile);
inputs.error.get_or_insert(crate::tr!( inputs.error.get_or_insert(crate::tr!(
crate::i18n::Locale::from_headers(headers), crate::i18n::Locale::from_headers(headers),
"td-err-not-a-table", "td-err-not-a-table",
@@ -269,7 +274,7 @@ pub(crate) async fn load_page(
// The append panel is held to the rules of the table it is appending to: a // The append panel is held to the rules of the table it is appending to: a
// shared table keeps no quantity ledger, and no link may point at the table // shared table keeps no quantity ledger, and no link may point at the table
// itself. // itself.
inputs.columns.global = inputs.selection.is_global() inputs.columns.global = inputs.selection.is_shared_profile(&shared_profile)
|| tables || tables
.iter() .iter()
.any(|table| table.name == inputs.selection.table && table.global); .any(|table| table.name == inputs.selection.table && table.global);
@@ -277,6 +282,7 @@ pub(crate) async fn load_page(
Ok(TableDefinitionPageState { Ok(TableDefinitionPageState {
nav: crate::ui::Nav::from_authorization(headers, "admin", &authorization), nav: crate::ui::Nav::from_authorization(headers, "admin", &authorization),
shared_profile,
tables, tables,
link_targets, link_targets,
detail, detail,

View File

@@ -11,8 +11,6 @@
use crate::schema::{ColumnCatalog, ColumnDraft}; use crate::schema::{ColumnCatalog, ColumnDraft};
pub(crate) use crate::pages::GLOBAL_SCOPE;
/// 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.
@@ -33,8 +31,10 @@ impl Selection {
!self.table.is_empty() !self.table.is_empty()
} }
pub(crate) fn is_global(&self) -> bool { /// Whether the selection is the shared profile, whose name the caller has
self.profile == GLOBAL_SCOPE /// from the profile tree — the page does not spell it itself.
pub(crate) fn is_shared_profile(&self, shared_profile: &str) -> bool {
self.profile == shared_profile
} }
/// 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
@@ -48,10 +48,14 @@ impl Selection {
format!("?profile={}", self.profile) format!("?profile={}", self.profile)
} }
/// What the heading calls the scope, so the global one is not shown by its /// What the heading calls the scope, so the shared profile is not shown by
/// sentinel name. /// its schema name.
pub(crate) fn scope_label(&self, locale: &crate::i18n::Locale) -> String { pub(crate) fn scope_label(
if self.is_global() { &self,
locale: &crate::i18n::Locale,
shared_profile: &str,
) -> String {
if self.is_shared_profile(shared_profile) {
crate::tr!(*locale, "td-global-label") crate::tr!(*locale, "td-global-label")
} else { } else {
self.profile.clone() self.profile.clone()
@@ -300,6 +304,10 @@ impl PageInputs {
pub(crate) struct TableDefinitionPageState { pub(crate) struct TableDefinitionPageState {
pub nav: crate::ui::Nav, pub nav: crate::ui::Nav,
pub selection: Selection, pub selection: Selection,
/// The profile the shared tables are stored in, as the profile tree
/// reports it. Everything that has to recognise the shared scope reads it
/// from here rather than knowing the name.
pub shared_profile: String,
/// The tables of the selected scope — what the workspace browses and acts /// The tables of the selected scope — what the workspace browses and acts
/// on. /// on.
pub tables: Vec<TableSummary>, pub tables: Vec<TableSummary>,
@@ -327,6 +335,17 @@ impl TableDefinitionPageState {
self.active == name self.active == name
} }
/// Whether the workspace is pointed at the shared profile rather than a
/// profile of its own. Read by context.html.
pub(crate) fn is_global(&self) -> bool {
self.selection.is_shared_profile(&self.shared_profile)
}
/// The heading's name for the selected scope. Read by context.html.
pub(crate) fn scope_label(&self, locale: &crate::i18n::Locale) -> String {
self.selection.scope_label(locale, &self.shared_profile)
}
/// A table cannot link to itself, and the chart of accounts is reached /// A table cannot link to itself, and the chart of accounts is reached
/// through an ACCOUNTING row rather than through a link of one's own. /// through an ACCOUNTING row rather than through a link of one's own.
fn eligible_link_target(&self, table: &TableSummary) -> bool { fn eligible_link_target(&self, table: &TableSummary) -> bool {

View File

@@ -256,9 +256,14 @@ mod tests {
} }
} }
/// What this deployment's server happens to call the shared profile. The
/// page is handed the name rather than knowing it.
const SHARED_PROFILE: &str = "__global";
fn page() -> TableDefinitionPageState { fn page() -> TableDefinitionPageState {
TableDefinitionPageState { TableDefinitionPageState {
nav: Nav::default(), nav: Nav::default(),
shared_profile: SHARED_PROFILE.to_string(),
selection: Selection { selection: Selection {
profile: "billing".to_string(), profile: "billing".to_string(),
table: "invoice".to_string(), table: "invoice".to_string(),
@@ -351,7 +356,7 @@ mod tests {
fn the_delete_page_accepts_a_global_table() { fn the_delete_page_accepts_a_global_table() {
let mut state = page(); let mut state = page();
state.selection = Selection { state.selection = Selection {
profile: crate::pages::GLOBAL_SCOPE.to_string(), profile: SHARED_PROFILE.to_string(),
table: "currencies".to_string(), table: "currencies".to_string(),
}; };
state.tables = vec![TableSummary { state.tables = vec![TableSummary {
@@ -661,7 +666,7 @@ mod tests {
#[test] #[test]
fn global_tables_have_their_own_scope() { fn global_tables_have_their_own_scope() {
let mut state = page(); let mut state = page();
state.selection.profile = "__global".to_string(); state.selection.profile = SHARED_PROFILE.to_string();
let html = render_presentation_page(&state); let html = render_presentation_page(&state);

View File

@@ -5,7 +5,7 @@ use crate::{
auth::GetAuthorizationRequest, auth::GetAuthorizationRequest,
definitions::{common::Empty, table_definition::GetTableCatalogRequest}, definitions::{common::Empty, table_definition::GetTableCatalogRequest},
i18n::Locale, i18n::Locale,
pages::{GLOBAL_SCOPE, table_scope}, pages::table_scope,
services::authenticated_request, services::authenticated_request,
}; };
@@ -16,8 +16,9 @@ pub(crate) struct Catalog {
/// One entry of the scope selector: the global scope, or a profile. /// One entry of the scope selector: the global scope, or a profile.
pub(crate) struct Profile { pub(crate) struct Profile {
/// What the form posts, and what the backend resolves the table in — /// What the form posts, and what the backend resolves the table in — the
/// `__global` for the shared tables, the schema name otherwise. /// schema name, which for the shared tables is the profile the server
/// reports as holding them.
pub name: String, pub name: String,
/// What the selector shows. The global scope is not a schema a user names, /// What the selector shows. The global scope is not a schema a user names,
/// so it is labelled in their language instead. /// so it is labelled in their language instead.
@@ -69,14 +70,16 @@ pub(crate) async fn load_catalog(
// A global table is shared by every profile, so the tree repeats it under // A global table is shared by every profile, so the tree repeats it under
// each one. Listing it there would offer the same table once per profile // each one. Listing it there would offer the same table once per profile
// and make "the tables of this profile" mean two different things; it is // and make "the tables of this profile" mean two different things; it is
// offered under the global scope instead, which — like every other page — // offered under the shared profile instead — named by the tree, since only
// is read from the catalog rather than derived from the tree. See // the server knows it — whose tables, like on every other page, are read
// from the catalog rather than derived from the tree. See
// `crate::pages::table_scope`. // `crate::pages::table_scope`.
let shared_profile = tree.shared_profile_name.clone();
let global = table_scope::global_tables( let global = table_scope::global_tables(
&definitions &definitions
.get_table_catalog( .get_table_catalog(
authenticated_request(headers, GetTableCatalogRequest { authenticated_request(headers, GetTableCatalogRequest {
profile_name: crate::pages::GLOBAL_SCOPE.to_string(), profile_name: shared_profile.clone(),
}) })
.map_err(|_| LoadError::Unauthenticated)?, .map_err(|_| LoadError::Unauthenticated)?,
) )
@@ -87,15 +90,15 @@ pub(crate) async fn load_catalog(
) )
.into_iter() .into_iter()
.map(|table| table.name) .map(|table| table.name)
.filter(|table| permits(GLOBAL_SCOPE, table)) .filter(|table| permits(&shared_profile, table))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let locale = Locale::from_headers(headers); let locale = Locale::from_headers(headers);
let mut profiles = Vec::new(); let mut profiles = Vec::new();
if !global.is_empty() { if !global.is_empty() {
profiles.push(Profile { profiles.push(Profile {
name: GLOBAL_SCOPE.to_string(), label: scope_label(locale, &shared_profile, &shared_profile),
label: scope_label(locale, GLOBAL_SCOPE), name: shared_profile.clone(),
tables: global, tables: global,
}); });
} }
@@ -146,7 +149,7 @@ pub(crate) async fn load_catalog(
} else { } else {
profiles.push(Profile { profiles.push(Profile {
name: profile_name.to_string(), name: profile_name.to_string(),
label: scope_label(locale, profile_name), label: scope_label(locale, profile_name, &shared_profile),
tables: vec![table_name.to_string()], tables: vec![table_name.to_string()],
}); });
} }
@@ -157,10 +160,10 @@ pub(crate) async fn load_catalog(
}) })
} }
/// A profile shows its schema name; the global scope is not one a user named, /// A profile shows its schema name; the shared profile is not one a user
/// so it is labelled in their language. /// named, so it is labelled in their language.
fn scope_label(locale: Locale, name: &str) -> String { fn scope_label(locale: Locale, name: &str, shared_profile: &str) -> String {
if name == GLOBAL_SCOPE { if name == shared_profile {
crate::tr!(locale, "admin-global-label") crate::tr!(locale, "admin-global-label")
} else { } else {
name.to_string() name.to_string()

View File

@@ -1,10 +1,3 @@
/// 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_logic;
pub(crate) mod add_table; pub(crate) mod add_table;
pub(crate) mod add_validation; pub(crate) mod add_validation;

View File

@@ -63,7 +63,10 @@ pub(crate) fn linkable_tables(
#[cfg(test)] #[cfg(test)]
pub(crate) mod tests { pub(crate) mod tests {
use super::*; use super::*;
use crate::pages::GLOBAL_SCOPE;
/// What this deployment's server happens to call the shared profile; the
/// tables carry it, and nothing here composes it.
const SHARED_PROFILE: &str = "__global";
pub(crate) fn table(name: &str, global: bool) -> Table { pub(crate) fn table(name: &str, global: bool) -> Table {
Table { Table {
@@ -74,7 +77,7 @@ pub(crate) mod tests {
table_kind: "dynamic".to_string(), table_kind: "dynamic".to_string(),
global, global,
profile_name: if global { profile_name: if global {
GLOBAL_SCOPE.to_string() SHARED_PROFILE.to_string()
} else { } else {
"test_profile".to_string() "test_profile".to_string()
}, },

View File

@@ -12,10 +12,10 @@
<section class="heading"> <section class="heading">
<div> <div>
<p class="eyebrow"> <p class="eyebrow">
{%- if page.selection.has_table() %}{{ page.selection.scope_label(nav.locale) }}{% else %}{{ nav.tr("td-eyebrow-table-definition") }}{% endif -%} {%- if page.selection.has_table() %}{{ page.scope_label(nav.locale) }}{% else %}{{ nav.tr("td-eyebrow-table-definition") }}{% endif -%}
</p> </p>
<h1> <h1>
{%- if page.selection.has_table() %}{{ page.selection.table }}{% else %}{{ page.selection.scope_label(nav.locale) }}{% endif -%} {%- if page.selection.has_table() %}{{ page.selection.table }}{% else %}{{ page.scope_label(nav.locale) }}{% endif -%}
</h1> </h1>
<p> <p>
{%- if let Some(detail) = page.detail -%} {%- if let Some(detail) = page.detail -%}
@@ -53,7 +53,7 @@
<span>{{ nav.tr("td-tab-access") }}</span><small>{{ nav.tr("td-tab-access-sub") }}</small> <span>{{ nav.tr("td-tab-access") }}</span><small>{{ nav.tr("td-tab-access-sub") }}</small>
</a> </a>
{% endif %} {% endif %}
{% if page.selection.has_profile() && !page.selection.is_global() %} {% if page.selection.has_profile() && !page.is_global() %}
<a href="/admin/profiles/copy{{ page.selection.profile_query() }}" class="tab {% if page.is("copy") %}selected{% endif %}" {% if page.is("copy") %}aria-current="page"{% endif %}> <a href="/admin/profiles/copy{{ page.selection.profile_query() }}" class="tab {% if page.is("copy") %}selected{% endif %}" {% if page.is("copy") %}aria-current="page"{% endif %}>
<span>{{ nav.tr("td-tab-copy") }}</span><small>{{ nav.tr("td-tab-copy-sub") }}</small> <span>{{ nav.tr("td-tab-copy") }}</span><small>{{ nav.tr("td-tab-copy-sub") }}</small>
</a> </a>