global table profile name from server in web now
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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()),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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()
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user