feat: reworked the category editing
This commit is contained in:
@@ -16,6 +16,7 @@ The project is under active development. Core blogging workflows are broadly ava
|
|||||||
- SQLite and filesystem persistence with byte-canonical bDS2 frontmatter, media sidecars, metadata JSON, and OPML menus; rebuild; bidirectional metadata diff/repair including project categories and publishing preferences; stale post-path cleanup on republish; bDS2-compatible checksums and NFD slug generation; and FTS5 search.
|
- SQLite and filesystem persistence with byte-canonical bDS2 frontmatter, media sidecars, metadata JSON, and OPML menus; rebuild; bidirectional metadata diff/repair including project categories and publishing preferences; stale post-path cleanup on republish; bDS2-compatible checksums and NFD slug generation; and FTS5 search.
|
||||||
- Optional on-device multilingual semantic search and similar-post tag suggestions backed by a persistent USearch index, plus always-available partial-name tag autocomplete and duplicate-post review in the desktop workspace.
|
- Optional on-device multilingual semantic search and similar-post tag suggestions backed by a persistent USearch index, plus always-available partial-name tag autocomplete and duplicate-post review in the desktop workspace.
|
||||||
- Read-only in-app browsers in the Help menu for the bundled global `DOCUMENTATION.md`, the generated Lua API reference with public types and runnable examples, the [CLI/server/TUI documentation](CLI.md), and the [MCP server documentation](MCP.md), with safe GFM rendering and confirmed external links.
|
- Read-only in-app browsers in the Help menu for the bundled global `DOCUMENTATION.md`, the generated Lua API reference with public types and runnable examples, the [CLI/server/TUI documentation](CLI.md), and the [MCP server documentation](MCP.md), with safe GFM rendering and confirmed external links.
|
||||||
|
- A localized Tags workspace manages tags and category settings; its category table includes the main language and every configured translation, whose titles are used by the matching category archives.
|
||||||
- A localized OPML menu editor manages pages, submenus, and category archives with protected Home ordering, keyboard-accessible tree controls, drag-and-drop, and bDS2-compatible persistence.
|
- A localized OPML menu editor manages pages, submenus, and category archives with protected Home ordering, keyboard-accessible tree controls, drag-and-drop, and bDS2-compatible persistence.
|
||||||
- Project-scoped typed domain events synchronize desktop views with shared-engine and future CLI mutations; persisted CLI notifications are consumed once, and the selected UI language is shared through settings.
|
- Project-scoped typed domain events synchronize desktop views with shared-engine and future CLI mutations; persisted CLI notifications are consumed once, and the selected UI language is shared through settings.
|
||||||
- Headless `bds-cli` automation for rebuild/repair/render, publishing and Git sync, post/media/gallery creation, effective shared settings with secret-presence redaction, projects, utility Lua tasks, JSON I/O, airplane-mode AI routing, and guarded launcher installation from Settings → Data or `bds-cli install`.
|
- Headless `bds-cli` automation for rebuild/repair/render, publishing and Git sync, post/media/gallery creation, effective shared settings with secret-presence redaction, projects, utility Lua tasks, JSON I/O, airplane-mode AI routing, and guarded launcher installation from Settings → Data or `bds-cli install`.
|
||||||
|
|||||||
@@ -446,6 +446,7 @@ pub fn add_category(
|
|||||||
category.to_string(),
|
category.to_string(),
|
||||||
CategorySettings {
|
CategorySettings {
|
||||||
title: None,
|
title: None,
|
||||||
|
titles: BTreeMap::new(),
|
||||||
render_in_lists: true,
|
render_in_lists: true,
|
||||||
show_title: true,
|
show_title: true,
|
||||||
post_template_slug: None,
|
post_template_slug: None,
|
||||||
@@ -642,6 +643,7 @@ mod tests {
|
|||||||
"article".to_string(),
|
"article".to_string(),
|
||||||
CategorySettings {
|
CategorySettings {
|
||||||
title: None,
|
title: None,
|
||||||
|
titles: BTreeMap::new(),
|
||||||
render_in_lists: true,
|
render_in_lists: true,
|
||||||
show_title: true,
|
show_title: true,
|
||||||
post_template_slug: None,
|
post_template_slug: None,
|
||||||
@@ -662,6 +664,7 @@ mod tests {
|
|||||||
"news".to_string(),
|
"news".to_string(),
|
||||||
CategorySettings {
|
CategorySettings {
|
||||||
title: Some("News Archive".to_string()),
|
title: Some("News Archive".to_string()),
|
||||||
|
titles: BTreeMap::from([("de".into(), "Nachrichten".into())]),
|
||||||
render_in_lists: false,
|
render_in_lists: false,
|
||||||
show_title: true,
|
show_title: true,
|
||||||
post_template_slug: Some("article".to_string()),
|
post_template_slug: Some("article".to_string()),
|
||||||
@@ -674,11 +677,13 @@ mod tests {
|
|||||||
let content = std::fs::read_to_string(dir.path().join("meta/category-meta.json")).unwrap();
|
let content = std::fs::read_to_string(dir.path().join("meta/category-meta.json")).unwrap();
|
||||||
let json: serde_json::Value = serde_json::from_str(&content).unwrap();
|
let json: serde_json::Value = serde_json::from_str(&content).unwrap();
|
||||||
assert_eq!(json["news"]["title"], "News Archive");
|
assert_eq!(json["news"]["title"], "News Archive");
|
||||||
|
assert_eq!(json["news"]["titles"]["de"], "Nachrichten");
|
||||||
assert_eq!(json["news"]["renderInLists"], false);
|
assert_eq!(json["news"]["renderInLists"], false);
|
||||||
assert_eq!(json["news"]["showTitle"], true);
|
assert_eq!(json["news"]["showTitle"], true);
|
||||||
|
|
||||||
let read = read_category_meta_json(dir.path()).unwrap();
|
let read = read_category_meta_json(dir.path()).unwrap();
|
||||||
assert_eq!(read["news"].title.as_deref(), Some("News Archive"));
|
assert_eq!(read["news"].title.as_deref(), Some("News Archive"));
|
||||||
|
assert_eq!(read["news"].title_for("de", "en"), Some("Nachrichten"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -689,6 +694,7 @@ mod tests {
|
|||||||
"zebra".to_string(),
|
"zebra".to_string(),
|
||||||
CategorySettings {
|
CategorySettings {
|
||||||
title: Some("Zebra".into()),
|
title: Some("Zebra".into()),
|
||||||
|
titles: BTreeMap::new(),
|
||||||
render_in_lists: true,
|
render_in_lists: true,
|
||||||
show_title: false,
|
show_title: false,
|
||||||
post_template_slug: Some("post".into()),
|
post_template_slug: Some("post".into()),
|
||||||
@@ -699,6 +705,7 @@ mod tests {
|
|||||||
"alpha".to_string(),
|
"alpha".to_string(),
|
||||||
CategorySettings {
|
CategorySettings {
|
||||||
title: None,
|
title: None,
|
||||||
|
titles: BTreeMap::new(),
|
||||||
render_in_lists: false,
|
render_in_lists: false,
|
||||||
show_title: true,
|
show_title: true,
|
||||||
post_template_slug: None,
|
post_template_slug: None,
|
||||||
@@ -903,6 +910,7 @@ mod tests {
|
|||||||
"article".to_string(),
|
"article".to_string(),
|
||||||
CategorySettings {
|
CategorySettings {
|
||||||
title: None,
|
title: None,
|
||||||
|
titles: BTreeMap::new(),
|
||||||
render_in_lists: true,
|
render_in_lists: true,
|
||||||
show_title: true,
|
show_title: true,
|
||||||
post_template_slug: None,
|
post_template_slug: None,
|
||||||
|
|||||||
@@ -1427,6 +1427,7 @@ mod tests {
|
|||||||
"news".to_string(),
|
"news".to_string(),
|
||||||
crate::model::metadata::CategorySettings {
|
crate::model::metadata::CategorySettings {
|
||||||
title: Some("News".into()),
|
title: Some("News".into()),
|
||||||
|
titles: std::collections::BTreeMap::new(),
|
||||||
render_in_lists: false,
|
render_in_lists: false,
|
||||||
show_title: true,
|
show_title: true,
|
||||||
post_template_slug: Some("article".into()),
|
post_template_slug: Some("article".into()),
|
||||||
|
|||||||
@@ -1546,6 +1546,7 @@ mod tests {
|
|||||||
"hidden".to_string(),
|
"hidden".to_string(),
|
||||||
crate::model::CategorySettings {
|
crate::model::CategorySettings {
|
||||||
title: None,
|
title: None,
|
||||||
|
titles: std::collections::BTreeMap::new(),
|
||||||
render_in_lists: false,
|
render_in_lists: false,
|
||||||
show_title: true,
|
show_title: true,
|
||||||
post_template_slug: None,
|
post_template_slug: None,
|
||||||
@@ -1556,6 +1557,7 @@ mod tests {
|
|||||||
"featured".to_string(),
|
"featured".to_string(),
|
||||||
crate::model::CategorySettings {
|
crate::model::CategorySettings {
|
||||||
title: None,
|
title: None,
|
||||||
|
titles: std::collections::BTreeMap::new(),
|
||||||
render_in_lists: true,
|
render_in_lists: true,
|
||||||
show_title: false,
|
show_title: false,
|
||||||
post_template_slug: None,
|
post_template_slug: None,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
pub const SUPPORTED_PICO_THEMES: [&str; 20] = [
|
pub const SUPPORTED_PICO_THEMES: [&str; 20] = [
|
||||||
@@ -107,6 +109,23 @@ pub struct CategorySettings {
|
|||||||
pub show_title: bool,
|
pub show_title: bool,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub title: Option<String>,
|
pub title: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||||
|
pub titles: BTreeMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CategorySettings {
|
||||||
|
pub fn title_for(&self, language: &str, main_language: &str) -> Option<&str> {
|
||||||
|
if language.eq_ignore_ascii_case(main_language) {
|
||||||
|
self.title.as_deref()
|
||||||
|
} else {
|
||||||
|
self.titles
|
||||||
|
.iter()
|
||||||
|
.find(|(candidate, _)| candidate.eq_ignore_ascii_case(language))
|
||||||
|
.map(|(_, title)| title.as_str())
|
||||||
|
}
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|title| !title.is_empty())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -177,6 +196,7 @@ mod tests {
|
|||||||
fn category_settings_camel_case() {
|
fn category_settings_camel_case() {
|
||||||
let settings = CategorySettings {
|
let settings = CategorySettings {
|
||||||
title: Some("Article".into()),
|
title: Some("Article".into()),
|
||||||
|
titles: BTreeMap::new(),
|
||||||
render_in_lists: false,
|
render_in_lists: false,
|
||||||
show_title: true,
|
show_title: true,
|
||||||
post_template_slug: Some("article-tpl".into()),
|
post_template_slug: Some("article-tpl".into()),
|
||||||
@@ -189,6 +209,22 @@ mod tests {
|
|||||||
assert!(json.contains("postTemplateSlug"));
|
assert!(json.contains("postTemplateSlug"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn category_settings_select_title_for_render_language() {
|
||||||
|
let settings = CategorySettings {
|
||||||
|
title: Some("Artikel".into()),
|
||||||
|
titles: std::collections::BTreeMap::from([("en".into(), "Articles".into())]),
|
||||||
|
render_in_lists: true,
|
||||||
|
show_title: true,
|
||||||
|
post_template_slug: None,
|
||||||
|
list_template_slug: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(settings.title_for("de", "de"), Some("Artikel"));
|
||||||
|
assert_eq!(settings.title_for("en", "de"), Some("Articles"));
|
||||||
|
assert_eq!(settings.title_for("fr", "de"), None);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn tag_entry_roundtrip() {
|
fn tag_entry_roundtrip() {
|
||||||
let tag = TagEntry {
|
let tag = TagEntry {
|
||||||
|
|||||||
@@ -921,9 +921,7 @@ fn build_language_routes(
|
|||||||
let slug = slugify(&category);
|
let slug = slugify(&category);
|
||||||
let display_name = category_settings
|
let display_name = category_settings
|
||||||
.get(&category)
|
.get(&category)
|
||||||
.and_then(|settings| settings.title.as_deref())
|
.and_then(|settings| settings.title_for(language, main_language(metadata)))
|
||||||
.map(str::trim)
|
|
||||||
.filter(|title| !title.is_empty())
|
|
||||||
.unwrap_or(&category);
|
.unwrap_or(&category);
|
||||||
routes.extend(paginated_route_specs(
|
routes.extend(paginated_route_specs(
|
||||||
&records,
|
&records,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::{BTreeMap, HashMap};
|
||||||
|
|
||||||
use bds_core::db::Database;
|
use bds_core::db::Database;
|
||||||
use bds_core::db::queries::project::insert_project;
|
use bds_core::db::queries::project::insert_project;
|
||||||
@@ -254,6 +254,21 @@ fn mixed_source_languages_use_the_main_language_at_canonical_urls() {
|
|||||||
metadata.main_language = Some("de".into());
|
metadata.main_language = Some("de".into());
|
||||||
metadata.blog_languages = vec!["de".into(), "en".into()];
|
metadata.blog_languages = vec!["de".into(), "en".into()];
|
||||||
bds_core::engine::meta::write_project_json(dir.path(), &metadata).unwrap();
|
bds_core::engine::meta::write_project_json(dir.path(), &metadata).unwrap();
|
||||||
|
write_category_meta_json(
|
||||||
|
dir.path(),
|
||||||
|
&HashMap::from([(
|
||||||
|
"article".into(),
|
||||||
|
CategorySettings {
|
||||||
|
title: Some("Artikel".into()),
|
||||||
|
titles: BTreeMap::from([("en".into(), "Articles".into())]),
|
||||||
|
render_in_lists: true,
|
||||||
|
show_title: true,
|
||||||
|
post_template_slug: None,
|
||||||
|
list_template_slug: None,
|
||||||
|
},
|
||||||
|
)]),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
insert_template(
|
insert_template(
|
||||||
db.conn(),
|
db.conn(),
|
||||||
&Template {
|
&Template {
|
||||||
@@ -391,6 +406,12 @@ fn mixed_source_languages_use_the_main_language_at_canonical_urls() {
|
|||||||
let english_index = std::fs::read_to_string(output.join("en/index.html")).unwrap();
|
let english_index = std::fs::read_to_string(output.join("en/index.html")).unwrap();
|
||||||
assert!(english_index.contains("English translation body"));
|
assert!(english_index.contains("English translation body"));
|
||||||
assert!(!english_index.contains(">English translation</a></h2>"));
|
assert!(!english_index.contains(">English translation</a></h2>"));
|
||||||
|
let german_category =
|
||||||
|
std::fs::read_to_string(output.join("category/article/index.html")).unwrap();
|
||||||
|
let english_category =
|
||||||
|
std::fs::read_to_string(output.join("en/category/article/index.html")).unwrap();
|
||||||
|
assert!(german_category.contains("<h1 class=\"archive-heading\">Artikel</h1>"));
|
||||||
|
assert!(english_category.contains("<h1 class=\"archive-heading\">Articles</h1>"));
|
||||||
assert!(
|
assert!(
|
||||||
output
|
output
|
||||||
.join("2024/03/12/german-private/index.html")
|
.join("2024/03/12/german-private/index.html")
|
||||||
@@ -725,6 +746,7 @@ fn generation_respects_category_list_settings_and_writes_bundled_images() {
|
|||||||
"hidden".to_string(),
|
"hidden".to_string(),
|
||||||
CategorySettings {
|
CategorySettings {
|
||||||
title: None,
|
title: None,
|
||||||
|
titles: BTreeMap::new(),
|
||||||
render_in_lists: false,
|
render_in_lists: false,
|
||||||
show_title: true,
|
show_title: true,
|
||||||
post_template_slug: None,
|
post_template_slug: None,
|
||||||
@@ -735,6 +757,7 @@ fn generation_respects_category_list_settings_and_writes_bundled_images() {
|
|||||||
"featured".to_string(),
|
"featured".to_string(),
|
||||||
CategorySettings {
|
CategorySettings {
|
||||||
title: Some("Featured Archive".to_string()),
|
title: Some("Featured Archive".to_string()),
|
||||||
|
titles: BTreeMap::from([("de".into(), "Ausgewählt".into())]),
|
||||||
render_in_lists: true,
|
render_in_lists: true,
|
||||||
show_title: false,
|
show_title: false,
|
||||||
post_template_slug: None,
|
post_template_slug: None,
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ pub enum Message {
|
|||||||
ToggleSidebar,
|
ToggleSidebar,
|
||||||
TogglePanel,
|
TogglePanel,
|
||||||
OpenSettingsSection(crate::views::settings_view::SettingsSection),
|
OpenSettingsSection(crate::views::settings_view::SettingsSection),
|
||||||
|
OpenTagsSection(TagsSection),
|
||||||
|
|
||||||
// Sidebar resize
|
// Sidebar resize
|
||||||
SidebarResizeStart,
|
SidebarResizeStart,
|
||||||
@@ -1678,6 +1679,22 @@ impl BdsApp {
|
|||||||
self.sync_menu_state();
|
self.sync_menu_state();
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
Message::OpenTagsSection(section) => {
|
||||||
|
self.sidebar_view = SidebarView::Tags;
|
||||||
|
self.sidebar_visible = true;
|
||||||
|
self.open_singleton_tab(TabType::Tags, "tabBar.tags");
|
||||||
|
if self.settings_state.is_none() {
|
||||||
|
self.settings_state = Some(self.hydrate_settings_state());
|
||||||
|
}
|
||||||
|
if self.tags_view_state.is_none() {
|
||||||
|
self.reload_tags_state();
|
||||||
|
}
|
||||||
|
if let Some(state) = self.tags_view_state.as_mut() {
|
||||||
|
state.section = section;
|
||||||
|
}
|
||||||
|
self.sync_menu_state();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
Message::SidebarResizeStart => {
|
Message::SidebarResizeStart => {
|
||||||
self.sidebar_dragging = true;
|
self.sidebar_dragging = true;
|
||||||
Task::none()
|
Task::none()
|
||||||
@@ -7582,6 +7599,9 @@ impl BdsApp {
|
|||||||
title: meta
|
title: meta
|
||||||
.and_then(|value| value.title.clone())
|
.and_then(|value| value.title.clone())
|
||||||
.unwrap_or_else(|| name.clone()),
|
.unwrap_or_else(|| name.clone()),
|
||||||
|
translated_titles: meta
|
||||||
|
.map(|value| value.titles.clone())
|
||||||
|
.unwrap_or_default(),
|
||||||
render_in_lists: meta.map(|value| value.render_in_lists).unwrap_or(true),
|
render_in_lists: meta.map(|value| value.render_in_lists).unwrap_or(true),
|
||||||
show_title: meta.map(|value| value.show_title).unwrap_or(true),
|
show_title: meta.map(|value| value.show_title).unwrap_or(true),
|
||||||
post_template_slug: meta
|
post_template_slug: meta
|
||||||
@@ -8225,9 +8245,13 @@ impl BdsApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SettingsMsg::CategoryTitleChanged(name, value) => {
|
SettingsMsg::CategoryTitleChanged(name, language, value) => {
|
||||||
if let Some(row) = state.categories.iter_mut().find(|row| row.name == name) {
|
if let Some(row) = state.categories.iter_mut().find(|row| row.name == name) {
|
||||||
row.title = value;
|
if language.eq_ignore_ascii_case(&state.main_language) {
|
||||||
|
row.title = value;
|
||||||
|
} else {
|
||||||
|
row.translated_titles.insert(language, value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SettingsMsg::CategoryRenderInListsChanged(name, value) => {
|
SettingsMsg::CategoryRenderInListsChanged(name, value) => {
|
||||||
@@ -8261,6 +8285,15 @@ impl BdsApp {
|
|||||||
row.name.clone(),
|
row.name.clone(),
|
||||||
bds_core::model::metadata::CategorySettings {
|
bds_core::model::metadata::CategorySettings {
|
||||||
title: Some(row.title.clone()).filter(|title| !title.trim().is_empty()),
|
title: Some(row.title.clone()).filter(|title| !title.trim().is_empty()),
|
||||||
|
titles: row
|
||||||
|
.translated_titles
|
||||||
|
.iter()
|
||||||
|
.filter(|(language, title)| {
|
||||||
|
!language.eq_ignore_ascii_case(&state.main_language)
|
||||||
|
&& !title.trim().is_empty()
|
||||||
|
})
|
||||||
|
.map(|(language, title)| (language.clone(), title.clone()))
|
||||||
|
.collect(),
|
||||||
render_in_lists: row.render_in_lists,
|
render_in_lists: row.render_in_lists,
|
||||||
show_title: row.show_title,
|
show_title: row.show_title,
|
||||||
post_template_slug: (!row.post_template_slug.is_empty())
|
post_template_slug: (!row.post_template_slug.is_empty())
|
||||||
@@ -8313,6 +8346,7 @@ impl BdsApp {
|
|||||||
bds_core::model::metadata::CategorySettings {
|
bds_core::model::metadata::CategorySettings {
|
||||||
title: Some(row.title.clone())
|
title: Some(row.title.clone())
|
||||||
.filter(|title| !title.trim().is_empty()),
|
.filter(|title| !title.trim().is_empty()),
|
||||||
|
titles: std::collections::BTreeMap::new(),
|
||||||
render_in_lists: row.render_in_lists,
|
render_in_lists: row.render_in_lists,
|
||||||
show_title: row.show_title,
|
show_title: row.show_title,
|
||||||
post_template_slug: None,
|
post_template_slug: None,
|
||||||
@@ -9047,6 +9081,9 @@ impl BdsApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
TabType::Tags => {
|
TabType::Tags => {
|
||||||
|
if self.settings_state.is_none() {
|
||||||
|
self.settings_state = Some(self.hydrate_settings_state());
|
||||||
|
}
|
||||||
if self.tags_view_state.is_none() {
|
if self.tags_view_state.is_none() {
|
||||||
let project_id = self
|
let project_id = self
|
||||||
.active_project
|
.active_project
|
||||||
@@ -13048,6 +13085,35 @@ mod tests {
|
|||||||
assert!(meta.semantic_similarity_enabled);
|
assert!(meta.semantic_similarity_enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn save_category_persists_main_and_translated_titles() {
|
||||||
|
let (db, project, tmp) = setup();
|
||||||
|
let mut app = make_app(db, project, &tmp);
|
||||||
|
let mut state = app.hydrate_settings_state();
|
||||||
|
state.main_language = "de".into();
|
||||||
|
state.blog_languages = vec!["de".into(), "en".into()];
|
||||||
|
app.settings_state = Some(state);
|
||||||
|
|
||||||
|
let _ = app.handle_settings_msg(SettingsMsg::CategoryTitleChanged(
|
||||||
|
"article".into(),
|
||||||
|
"de".into(),
|
||||||
|
"Artikel".into(),
|
||||||
|
));
|
||||||
|
let _ = app.handle_settings_msg(SettingsMsg::CategoryTitleChanged(
|
||||||
|
"article".into(),
|
||||||
|
"en".into(),
|
||||||
|
"Articles".into(),
|
||||||
|
));
|
||||||
|
let _ = app.handle_settings_msg(SettingsMsg::SaveCategory("article".into()));
|
||||||
|
|
||||||
|
let categories = bds_core::engine::meta::read_category_meta_json(tmp.path()).unwrap();
|
||||||
|
assert_eq!(categories["article"].title.as_deref(), Some("Artikel"));
|
||||||
|
assert_eq!(
|
||||||
|
categories["article"].title_for("en", "de"),
|
||||||
|
Some("Articles")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn add_category_and_reset_defaults_updates_metadata_files() {
|
fn add_category_and_reset_defaults_updates_metadata_files() {
|
||||||
let (db, project, tmp) = setup();
|
let (db, project, tmp) = setup();
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ pub fn text_editor_style(_theme: &Theme, status: text_editor::Status) -> text_ed
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn select_style(_theme: &Theme, status: pick_list::Status) -> pick_list::Style {
|
pub(crate) fn select_style(_theme: &Theme, status: pick_list::Status) -> pick_list::Style {
|
||||||
let border_color = match status {
|
let border_color = match status {
|
||||||
pick_list::Status::Hovered | pick_list::Status::Opened => FOCUS_COLOR,
|
pick_list::Status::Hovered | pick_list::Status::Opened => FOCUS_COLOR,
|
||||||
pick_list::Status::Active => BORDER_COLOR,
|
pick_list::Status::Active => BORDER_COLOR,
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
use iced::widget::text::Shaping;
|
use iced::widget::text::Shaping;
|
||||||
use iced::widget::{button, column, container, row, scrollable, text, text_editor, text_input};
|
use iced::widget::{button, column, container, row, scrollable, text, text_editor, text_input};
|
||||||
use iced::{Alignment, Color, Element, Length};
|
use iced::{Alignment, Color, Element, Length};
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use bds_core::engine::ai::AiEndpointKind;
|
use bds_core::engine::ai::AiEndpointKind;
|
||||||
use bds_core::i18n::UiLocale;
|
use bds_core::i18n::UiLocale;
|
||||||
|
|
||||||
use crate::app::Message;
|
use crate::app::Message;
|
||||||
use crate::components::inputs;
|
use crate::components::inputs;
|
||||||
use crate::i18n::{t, tw};
|
use crate::i18n::t;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct AiModelOption {
|
pub struct AiModelOption {
|
||||||
@@ -41,7 +42,6 @@ pub struct AiModeViewState {
|
|||||||
pub enum SettingsSection {
|
pub enum SettingsSection {
|
||||||
Project,
|
Project,
|
||||||
Editor,
|
Editor,
|
||||||
Content,
|
|
||||||
AI,
|
AI,
|
||||||
Technology,
|
Technology,
|
||||||
Publishing,
|
Publishing,
|
||||||
@@ -53,6 +53,7 @@ pub enum SettingsSection {
|
|||||||
pub struct SettingsCategoryRow {
|
pub struct SettingsCategoryRow {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
|
pub translated_titles: BTreeMap<String, String>,
|
||||||
pub render_in_lists: bool,
|
pub render_in_lists: bool,
|
||||||
pub show_title: bool,
|
pub show_title: bool,
|
||||||
pub post_template_slug: String,
|
pub post_template_slug: String,
|
||||||
@@ -82,6 +83,7 @@ pub fn default_category_rows() -> Vec<SettingsCategoryRow> {
|
|||||||
.map(|name| SettingsCategoryRow {
|
.map(|name| SettingsCategoryRow {
|
||||||
name: (*name).to_string(),
|
name: (*name).to_string(),
|
||||||
title: (*name).to_string(),
|
title: (*name).to_string(),
|
||||||
|
translated_titles: BTreeMap::new(),
|
||||||
render_in_lists: true,
|
render_in_lists: true,
|
||||||
show_title: true,
|
show_title: true,
|
||||||
post_template_slug: String::new(),
|
post_template_slug: String::new(),
|
||||||
@@ -96,7 +98,6 @@ impl SettingsSection {
|
|||||||
&[
|
&[
|
||||||
Self::Project,
|
Self::Project,
|
||||||
Self::Editor,
|
Self::Editor,
|
||||||
Self::Content,
|
|
||||||
Self::AI,
|
Self::AI,
|
||||||
Self::Technology,
|
Self::Technology,
|
||||||
Self::Publishing,
|
Self::Publishing,
|
||||||
@@ -109,7 +110,6 @@ impl SettingsSection {
|
|||||||
match self {
|
match self {
|
||||||
Self::Project => "settings.nav.project",
|
Self::Project => "settings.nav.project",
|
||||||
Self::Editor => "settings.nav.editor",
|
Self::Editor => "settings.nav.editor",
|
||||||
Self::Content => "settings.nav.content",
|
|
||||||
Self::AI => "settings.nav.ai",
|
Self::AI => "settings.nav.ai",
|
||||||
Self::Technology => "settings.nav.technology",
|
Self::Technology => "settings.nav.technology",
|
||||||
Self::Publishing => "settings.nav.publishing",
|
Self::Publishing => "settings.nav.publishing",
|
||||||
@@ -141,7 +141,7 @@ pub struct SettingsViewState {
|
|||||||
pub diff_view_style: String,
|
pub diff_view_style: String,
|
||||||
pub wrap_long_lines: bool,
|
pub wrap_long_lines: bool,
|
||||||
pub hide_unchanged_regions: bool,
|
pub hide_unchanged_regions: bool,
|
||||||
// Content
|
// Categories (Tags workspace)
|
||||||
pub categories: Vec<SettingsCategoryRow>,
|
pub categories: Vec<SettingsCategoryRow>,
|
||||||
pub new_category_name: String,
|
pub new_category_name: String,
|
||||||
pub template_options: Vec<String>,
|
pub template_options: Vec<String>,
|
||||||
@@ -312,7 +312,7 @@ pub enum SettingsMsg {
|
|||||||
// Content
|
// Content
|
||||||
AddCategoryNameChanged(String),
|
AddCategoryNameChanged(String),
|
||||||
AddCategory,
|
AddCategory,
|
||||||
CategoryTitleChanged(String, String),
|
CategoryTitleChanged(String, String, String),
|
||||||
CategoryRenderInListsChanged(String, bool),
|
CategoryRenderInListsChanged(String, bool),
|
||||||
CategoryShowTitleChanged(String, bool),
|
CategoryShowTitleChanged(String, bool),
|
||||||
CategoryPostTemplateChanged(String, String),
|
CategoryPostTemplateChanged(String, String),
|
||||||
@@ -445,7 +445,6 @@ fn render_section<'a>(
|
|||||||
let content: Element<'a, Message> = match section {
|
let content: Element<'a, Message> = match section {
|
||||||
SettingsSection::Project => section_project(state, locale),
|
SettingsSection::Project => section_project(state, locale),
|
||||||
SettingsSection::Editor => section_editor(state, locale),
|
SettingsSection::Editor => section_editor(state, locale),
|
||||||
SettingsSection::Content => section_content(state, locale),
|
|
||||||
SettingsSection::AI => section_ai(state, locale),
|
SettingsSection::AI => section_ai(state, locale),
|
||||||
SettingsSection::Technology => section_technology(state, locale),
|
SettingsSection::Technology => section_technology(state, locale),
|
||||||
SettingsSection::Publishing => section_publishing(state, locale),
|
SettingsSection::Publishing => section_publishing(state, locale),
|
||||||
@@ -631,143 +630,6 @@ fn section_editor<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Element
|
|||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn section_content<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Element<'a, Message> {
|
|
||||||
let template_options = std::iter::once(String::new())
|
|
||||||
.chain(state.template_options.iter().cloned())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let category_rows = state
|
|
||||||
.categories
|
|
||||||
.iter()
|
|
||||||
.fold(column![].spacing(8), |column, category| {
|
|
||||||
let title = inputs::labeled_input(
|
|
||||||
&tw(
|
|
||||||
locale,
|
|
||||||
"settings.categoryTitle",
|
|
||||||
&[("category", &category.name)],
|
|
||||||
),
|
|
||||||
"",
|
|
||||||
&category.title,
|
|
||||||
{
|
|
||||||
let name = category.name.clone();
|
|
||||||
move |value| {
|
|
||||||
Message::Settings(SettingsMsg::CategoryTitleChanged(name.clone(), value))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
let post_template = inputs::labeled_select(
|
|
||||||
&t(locale, "settings.categoryPostTemplate"),
|
|
||||||
&template_options,
|
|
||||||
Some(&category.post_template_slug),
|
|
||||||
{
|
|
||||||
let name = category.name.clone();
|
|
||||||
move |value| {
|
|
||||||
Message::Settings(SettingsMsg::CategoryPostTemplateChanged(
|
|
||||||
name.clone(),
|
|
||||||
value,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
let list_template = inputs::labeled_select(
|
|
||||||
&t(locale, "settings.categoryListTemplate"),
|
|
||||||
&template_options,
|
|
||||||
Some(&category.list_template_slug),
|
|
||||||
{
|
|
||||||
let name = category.name.clone();
|
|
||||||
move |value| {
|
|
||||||
Message::Settings(SettingsMsg::CategoryListTemplateChanged(
|
|
||||||
name.clone(),
|
|
||||||
value,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
let toggles = column![
|
|
||||||
inputs::labeled_checkbox(
|
|
||||||
&t(locale, "settings.categoryRenderInLists"),
|
|
||||||
category.render_in_lists,
|
|
||||||
{
|
|
||||||
let name = category.name.clone();
|
|
||||||
move |value| {
|
|
||||||
Message::Settings(SettingsMsg::CategoryRenderInListsChanged(
|
|
||||||
name.clone(),
|
|
||||||
value,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
inputs::labeled_checkbox(
|
|
||||||
&t(locale, "settings.categoryShowTitles"),
|
|
||||||
category.show_title,
|
|
||||||
{
|
|
||||||
let name = category.name.clone();
|
|
||||||
move |value| {
|
|
||||||
Message::Settings(SettingsMsg::CategoryShowTitleChanged(
|
|
||||||
name.clone(),
|
|
||||||
value,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
.spacing(6);
|
|
||||||
let actions = row![
|
|
||||||
button(text(t(locale, "common.save")).size(13))
|
|
||||||
.on_press(Message::Settings(SettingsMsg::SaveCategory(
|
|
||||||
category.name.clone()
|
|
||||||
)))
|
|
||||||
.style(inputs::primary_button)
|
|
||||||
.padding([6, 12]),
|
|
||||||
button(text(t(locale, "common.remove")).size(13))
|
|
||||||
.on_press_maybe((!category.is_protected).then(|| Message::Settings(
|
|
||||||
SettingsMsg::RemoveCategory(category.name.clone())
|
|
||||||
)))
|
|
||||||
.style(inputs::danger_button)
|
|
||||||
.padding([6, 12]),
|
|
||||||
]
|
|
||||||
.spacing(8);
|
|
||||||
|
|
||||||
column.push(
|
|
||||||
inputs::card(
|
|
||||||
column![
|
|
||||||
text(category.name.clone()).size(15).color(Color::WHITE),
|
|
||||||
title,
|
|
||||||
toggles,
|
|
||||||
row![post_template, list_template].spacing(12),
|
|
||||||
actions,
|
|
||||||
]
|
|
||||||
.spacing(8),
|
|
||||||
)
|
|
||||||
.padding(12),
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
let add_row = row![
|
|
||||||
inputs::labeled_input(
|
|
||||||
&t(locale, "settings.addCategory"),
|
|
||||||
"news",
|
|
||||||
&state.new_category_name,
|
|
||||||
|value| Message::Settings(SettingsMsg::AddCategoryNameChanged(value)),
|
|
||||||
),
|
|
||||||
button(text(t(locale, "common.add")).size(13))
|
|
||||||
.on_press(Message::Settings(SettingsMsg::AddCategory))
|
|
||||||
.style(inputs::primary_button)
|
|
||||||
.padding([6, 12]),
|
|
||||||
button(text(t(locale, "settings.resetCategories")).size(13))
|
|
||||||
.on_press(Message::Settings(SettingsMsg::ResetCategoriesToDefaults))
|
|
||||||
.style(inputs::secondary_button)
|
|
||||||
.padding([6, 12]),
|
|
||||||
]
|
|
||||||
.spacing(8)
|
|
||||||
.align_y(Alignment::End);
|
|
||||||
|
|
||||||
column![category_rows, add_row]
|
|
||||||
.spacing(12)
|
|
||||||
.padding([0, 16])
|
|
||||||
.into()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn section_ai<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Element<'a, Message> {
|
fn section_ai<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Element<'a, Message> {
|
||||||
let online = ai_mode_block(
|
let online = ai_mode_block(
|
||||||
&state.online_ai,
|
&state.online_ai,
|
||||||
|
|||||||
@@ -1234,7 +1234,6 @@ pub fn view(
|
|||||||
let sections: &[(&str, Option<SettingsSection>)] = &[
|
let sections: &[(&str, Option<SettingsSection>)] = &[
|
||||||
("settings.nav.project", Some(SettingsSection::Project)),
|
("settings.nav.project", Some(SettingsSection::Project)),
|
||||||
("settings.nav.editor", Some(SettingsSection::Editor)),
|
("settings.nav.editor", Some(SettingsSection::Editor)),
|
||||||
("settings.nav.content", Some(SettingsSection::Content)),
|
|
||||||
("settings.nav.ai", Some(SettingsSection::AI)),
|
("settings.nav.ai", Some(SettingsSection::AI)),
|
||||||
("settings.nav.technology", Some(SettingsSection::Technology)),
|
("settings.nav.technology", Some(SettingsSection::Technology)),
|
||||||
("settings.nav.publishing", Some(SettingsSection::Publishing)),
|
("settings.nav.publishing", Some(SettingsSection::Publishing)),
|
||||||
@@ -1269,21 +1268,31 @@ pub fn view(
|
|||||||
iced::widget::Column::with_children(items).spacing(1).into()
|
iced::widget::Column::with_children(items).spacing(1).into()
|
||||||
}
|
}
|
||||||
SidebarView::Tags => {
|
SidebarView::Tags => {
|
||||||
// Per sidebar_views.allium TagsNav: 3 fixed-order sections
|
let sections = [
|
||||||
let sections = ["tags.nav.cloud", "tags.nav.manage", "tags.nav.merge"];
|
(
|
||||||
|
"tags.nav.cloud",
|
||||||
|
crate::views::tags_view::TagsSection::Cloud,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"tags.nav.manage",
|
||||||
|
crate::views::tags_view::TagsSection::Manage,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"tags.nav.merge",
|
||||||
|
crate::views::tags_view::TagsSection::Merge,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"tags.nav.categories",
|
||||||
|
crate::views::tags_view::TagsSection::Categories,
|
||||||
|
),
|
||||||
|
];
|
||||||
let items: Vec<Element<'static, Message>> = sections
|
let items: Vec<Element<'static, Message>> = sections
|
||||||
.iter()
|
.iter()
|
||||||
.map(|key| {
|
.map(|(key, section)| {
|
||||||
let label = t(locale, key);
|
let label = t(locale, key);
|
||||||
let label_text = text(label).size(12).shaping(Shaping::Advanced);
|
let label_text = text(label).size(12).shaping(Shaping::Advanced);
|
||||||
button(container(label_text).width(Length::Fill))
|
button(container(label_text).width(Length::Fill))
|
||||||
.on_press(Message::OpenTab(Tab {
|
.on_press(Message::OpenTagsSection(*section))
|
||||||
id: "tags".to_string(),
|
|
||||||
tab_type: TabType::Tags,
|
|
||||||
title: t(locale, "tabBar.tags"),
|
|
||||||
is_transient: false,
|
|
||||||
is_dirty: false,
|
|
||||||
}))
|
|
||||||
.padding([5, 8])
|
.padding([5, 8])
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.style(item_style)
|
.style(item_style)
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use iced::widget::{Space, button, column, container, row, scrollable, text, text_input};
|
use iced::widget::{
|
||||||
|
Space, button, checkbox, column, container, pick_list, row, scrollable, text, text_input,
|
||||||
|
};
|
||||||
use iced::{Alignment, Background, Color, Element, Length, Theme};
|
use iced::{Alignment, Background, Color, Element, Length, Theme};
|
||||||
|
|
||||||
use bds_core::i18n::UiLocale;
|
use bds_core::i18n::UiLocale;
|
||||||
@@ -9,6 +11,7 @@ use bds_core::model::Tag;
|
|||||||
use crate::app::Message;
|
use crate::app::Message;
|
||||||
use crate::components::inputs;
|
use crate::components::inputs;
|
||||||
use crate::i18n::{t, tw};
|
use crate::i18n::{t, tw};
|
||||||
|
use crate::views::settings_view::{SettingsMsg, SettingsViewState};
|
||||||
|
|
||||||
const DEFAULT_TAG_COLOR: &str = "#6495ed";
|
const DEFAULT_TAG_COLOR: &str = "#6495ed";
|
||||||
const COLOR_PRESETS: [&str; 17] = [
|
const COLOR_PRESETS: [&str; 17] = [
|
||||||
@@ -24,6 +27,7 @@ pub enum TagsSection {
|
|||||||
Manage,
|
Manage,
|
||||||
Merge,
|
Merge,
|
||||||
Discover,
|
Discover,
|
||||||
|
Categories,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// State for the tags view.
|
/// State for the tags view.
|
||||||
@@ -116,7 +120,11 @@ pub enum TagsMsg {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Render the tags management view.
|
/// Render the tags management view.
|
||||||
pub fn view<'a>(state: &'a TagsViewState, locale: UiLocale) -> Element<'a, Message> {
|
pub fn view<'a>(
|
||||||
|
state: &'a TagsViewState,
|
||||||
|
settings: &'a SettingsViewState,
|
||||||
|
locale: UiLocale,
|
||||||
|
) -> Element<'a, Message> {
|
||||||
let section_nav = inputs::card(
|
let section_nav = inputs::card(
|
||||||
row![
|
row![
|
||||||
section_tab(
|
section_tab(
|
||||||
@@ -139,6 +147,11 @@ pub fn view<'a>(state: &'a TagsViewState, locale: UiLocale) -> Element<'a, Messa
|
|||||||
state.section == TagsSection::Discover,
|
state.section == TagsSection::Discover,
|
||||||
TagsSection::Discover
|
TagsSection::Discover
|
||||||
),
|
),
|
||||||
|
section_tab(
|
||||||
|
&t(locale, "tags.nav.categories"),
|
||||||
|
state.section == TagsSection::Categories,
|
||||||
|
TagsSection::Categories
|
||||||
|
),
|
||||||
]
|
]
|
||||||
.spacing(6),
|
.spacing(6),
|
||||||
)
|
)
|
||||||
@@ -149,6 +162,7 @@ pub fn view<'a>(state: &'a TagsViewState, locale: UiLocale) -> Element<'a, Messa
|
|||||||
TagsSection::Manage => view_manage(state, locale),
|
TagsSection::Manage => view_manage(state, locale),
|
||||||
TagsSection::Merge => view_merge(state, locale),
|
TagsSection::Merge => view_merge(state, locale),
|
||||||
TagsSection::Discover => view_discover(state, locale),
|
TagsSection::Discover => view_discover(state, locale),
|
||||||
|
TagsSection::Categories => view_categories(settings, locale),
|
||||||
};
|
};
|
||||||
|
|
||||||
column![section_nav, content]
|
column![section_nav, content]
|
||||||
@@ -158,6 +172,215 @@ pub fn view<'a>(state: &'a TagsViewState, locale: UiLocale) -> Element<'a, Messa
|
|||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn view_categories<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Element<'a, Message> {
|
||||||
|
const NAME_WIDTH: f32 = 120.0;
|
||||||
|
const TITLE_WIDTH: f32 = 170.0;
|
||||||
|
const TOGGLE_WIDTH: f32 = 110.0;
|
||||||
|
const TEMPLATE_WIDTH: f32 = 150.0;
|
||||||
|
const ACTION_WIDTH: f32 = 150.0;
|
||||||
|
|
||||||
|
let mut languages = vec![state.main_language.clone()];
|
||||||
|
for language in &state.blog_languages {
|
||||||
|
if !languages
|
||||||
|
.iter()
|
||||||
|
.any(|existing| existing.eq_ignore_ascii_case(language))
|
||||||
|
{
|
||||||
|
languages.push(language.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let header_cell = |label: String, width| {
|
||||||
|
container(text(label).size(12).color(inputs::LABEL_COLOR))
|
||||||
|
.width(Length::Fixed(width))
|
||||||
|
.into()
|
||||||
|
};
|
||||||
|
let mut header: Vec<Element<'a, Message>> =
|
||||||
|
vec![header_cell(t(locale, "tags.name"), NAME_WIDTH)];
|
||||||
|
header.extend(languages.iter().map(|language| {
|
||||||
|
header_cell(
|
||||||
|
tw(locale, "categories.title", &[("language", language)]),
|
||||||
|
TITLE_WIDTH,
|
||||||
|
)
|
||||||
|
}));
|
||||||
|
header.extend([
|
||||||
|
header_cell(t(locale, "settings.categoryRenderInLists"), TOGGLE_WIDTH),
|
||||||
|
header_cell(t(locale, "settings.categoryShowTitles"), TOGGLE_WIDTH),
|
||||||
|
header_cell(t(locale, "settings.categoryPostTemplate"), TEMPLATE_WIDTH),
|
||||||
|
header_cell(t(locale, "settings.categoryListTemplate"), TEMPLATE_WIDTH),
|
||||||
|
header_cell(t(locale, "categories.actions"), ACTION_WIDTH),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let template_options = std::iter::once(String::new())
|
||||||
|
.chain(state.template_options.iter().cloned())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let mut table = column![iced::widget::Row::with_children(header).spacing(8)].spacing(8);
|
||||||
|
for category in &state.categories {
|
||||||
|
let mut cells: Vec<Element<'a, Message>> = vec![
|
||||||
|
container(text(category.name.clone()).size(13))
|
||||||
|
.width(Length::Fixed(NAME_WIDTH))
|
||||||
|
.into(),
|
||||||
|
];
|
||||||
|
cells.extend(languages.iter().map(|language| {
|
||||||
|
let value = if language.eq_ignore_ascii_case(&state.main_language) {
|
||||||
|
&category.title
|
||||||
|
} else {
|
||||||
|
category
|
||||||
|
.translated_titles
|
||||||
|
.get(language)
|
||||||
|
.map(String::as_str)
|
||||||
|
.unwrap_or("")
|
||||||
|
};
|
||||||
|
let name = category.name.clone();
|
||||||
|
let language = language.clone();
|
||||||
|
text_input("", value)
|
||||||
|
.on_input(move |value| {
|
||||||
|
Message::Settings(SettingsMsg::CategoryTitleChanged(
|
||||||
|
name.clone(),
|
||||||
|
language.clone(),
|
||||||
|
value,
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.padding([7, 9])
|
||||||
|
.style(inputs::field_style)
|
||||||
|
.width(Length::Fixed(TITLE_WIDTH))
|
||||||
|
.into()
|
||||||
|
}));
|
||||||
|
let name = category.name.clone();
|
||||||
|
cells.push(
|
||||||
|
container(
|
||||||
|
checkbox("", category.render_in_lists).on_toggle(move |value| {
|
||||||
|
Message::Settings(SettingsMsg::CategoryRenderInListsChanged(
|
||||||
|
name.clone(),
|
||||||
|
value,
|
||||||
|
))
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.width(Length::Fixed(TOGGLE_WIDTH))
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
let name = category.name.clone();
|
||||||
|
cells.push(
|
||||||
|
container(checkbox("", category.show_title).on_toggle(move |value| {
|
||||||
|
Message::Settings(SettingsMsg::CategoryShowTitleChanged(name.clone(), value))
|
||||||
|
}))
|
||||||
|
.width(Length::Fixed(TOGGLE_WIDTH))
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
let name = category.name.clone();
|
||||||
|
cells.push(
|
||||||
|
pick_list(
|
||||||
|
template_options.clone(),
|
||||||
|
Some(category.post_template_slug.clone()),
|
||||||
|
move |value| {
|
||||||
|
Message::Settings(SettingsMsg::CategoryPostTemplateChanged(
|
||||||
|
name.clone(),
|
||||||
|
value,
|
||||||
|
))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.padding([7, 9])
|
||||||
|
.style(inputs::select_style)
|
||||||
|
.width(Length::Fixed(TEMPLATE_WIDTH))
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
let name = category.name.clone();
|
||||||
|
cells.push(
|
||||||
|
pick_list(
|
||||||
|
template_options.clone(),
|
||||||
|
Some(category.list_template_slug.clone()),
|
||||||
|
move |value| {
|
||||||
|
Message::Settings(SettingsMsg::CategoryListTemplateChanged(
|
||||||
|
name.clone(),
|
||||||
|
value,
|
||||||
|
))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.padding([7, 9])
|
||||||
|
.style(inputs::select_style)
|
||||||
|
.width(Length::Fixed(TEMPLATE_WIDTH))
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
cells.push(
|
||||||
|
container(
|
||||||
|
row![
|
||||||
|
button(text(t(locale, "common.save")).size(12))
|
||||||
|
.on_press(Message::Settings(SettingsMsg::SaveCategory(
|
||||||
|
category.name.clone(),
|
||||||
|
)))
|
||||||
|
.style(inputs::primary_button)
|
||||||
|
.padding([6, 10]),
|
||||||
|
button(text(t(locale, "common.remove")).size(12))
|
||||||
|
.on_press_maybe((!category.is_protected).then(|| Message::Settings(
|
||||||
|
SettingsMsg::RemoveCategory(category.name.clone()),
|
||||||
|
)))
|
||||||
|
.style(inputs::danger_button)
|
||||||
|
.padding([6, 10]),
|
||||||
|
]
|
||||||
|
.spacing(6),
|
||||||
|
)
|
||||||
|
.width(Length::Fixed(ACTION_WIDTH))
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
table = table.push(
|
||||||
|
iced::widget::Row::with_children(cells)
|
||||||
|
.spacing(8)
|
||||||
|
.align_y(Alignment::Center),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let add = inputs::card(
|
||||||
|
row![
|
||||||
|
inputs::labeled_input(
|
||||||
|
&t(locale, "settings.addCategory"),
|
||||||
|
"news",
|
||||||
|
&state.new_category_name,
|
||||||
|
|value| Message::Settings(SettingsMsg::AddCategoryNameChanged(value)),
|
||||||
|
),
|
||||||
|
button(text(t(locale, "common.add")).size(13))
|
||||||
|
.on_press(Message::Settings(SettingsMsg::AddCategory))
|
||||||
|
.style(inputs::primary_button)
|
||||||
|
.padding([6, 12]),
|
||||||
|
button(text(t(locale, "settings.resetCategories")).size(13))
|
||||||
|
.on_press(Message::Settings(SettingsMsg::ResetCategoriesToDefaults))
|
||||||
|
.style(inputs::secondary_button)
|
||||||
|
.padding([6, 12]),
|
||||||
|
]
|
||||||
|
.spacing(8)
|
||||||
|
.align_y(Alignment::End),
|
||||||
|
);
|
||||||
|
let table_width = NAME_WIDTH
|
||||||
|
+ TITLE_WIDTH * languages.len() as f32
|
||||||
|
+ TOGGLE_WIDTH * 2.0
|
||||||
|
+ TEMPLATE_WIDTH * 2.0
|
||||||
|
+ ACTION_WIDTH
|
||||||
|
+ 8.0 * (languages.len() + 5) as f32
|
||||||
|
+ 24.0;
|
||||||
|
let table = scrollable(
|
||||||
|
inputs::card(table)
|
||||||
|
.padding(12)
|
||||||
|
.width(Length::Fixed(table_width)),
|
||||||
|
)
|
||||||
|
.direction(scrollable::Direction::Horizontal(
|
||||||
|
inputs::compact_scrollbar(),
|
||||||
|
))
|
||||||
|
.style(inputs::scrollable_style)
|
||||||
|
.width(Length::Fill);
|
||||||
|
|
||||||
|
scrollable(
|
||||||
|
column![
|
||||||
|
inputs::card(text(t(locale, "categories.section")).size(18)).padding(16),
|
||||||
|
add,
|
||||||
|
table,
|
||||||
|
]
|
||||||
|
.spacing(12)
|
||||||
|
.padding(16),
|
||||||
|
)
|
||||||
|
.direction(scrollable::Direction::Vertical(inputs::compact_scrollbar()))
|
||||||
|
.style(inputs::scrollable_style)
|
||||||
|
.height(Length::Fill)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
fn section_tab<'a>(label: &str, active: bool, section: TagsSection) -> Element<'a, Message> {
|
fn section_tab<'a>(label: &str, active: bool, section: TagsSection) -> Element<'a, Message> {
|
||||||
button(text(label.to_string()).size(13))
|
button(text(label.to_string()).size(13))
|
||||||
.on_press(Message::Tags(TagsMsg::SetSection(section)))
|
.on_press(Message::Tags(TagsMsg::SetSection(section)))
|
||||||
|
|||||||
@@ -506,8 +506,8 @@ fn route_content_area<'a>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ContentRoute::Tags => {
|
ContentRoute::Tags => {
|
||||||
if let Some(state) = tags_view_state {
|
if let (Some(state), Some(settings)) = (tags_view_state, settings_state) {
|
||||||
tags_view::view(state, locale)
|
tags_view::view(state, settings, locale)
|
||||||
} else {
|
} else {
|
||||||
loading_view(locale)
|
loading_view(locale)
|
||||||
}
|
}
|
||||||
@@ -646,7 +646,7 @@ fn route_kind<'a>(
|
|||||||
}
|
}
|
||||||
TabType::Chat => ContentRoute::Chat(tab_id),
|
TabType::Chat => ContentRoute::Chat(tab_id),
|
||||||
TabType::Tags => {
|
TabType::Tags => {
|
||||||
if tags_view_state.is_some() {
|
if tags_view_state.is_some() && settings_state.is_some() {
|
||||||
ContentRoute::Tags
|
ContentRoute::Tags
|
||||||
} else {
|
} else {
|
||||||
ContentRoute::Loading
|
ContentRoute::Loading
|
||||||
|
|||||||
@@ -330,6 +330,7 @@ tags-nav-cloud = Tag-Wolke
|
|||||||
tags-nav-manage = Tags verwalten
|
tags-nav-manage = Tags verwalten
|
||||||
tags-nav-merge = Tags zusammenführen
|
tags-nav-merge = Tags zusammenführen
|
||||||
tags-nav-discover = Tags entdecken
|
tags-nav-discover = Tags entdecken
|
||||||
|
tags-nav-categories = Kategorien
|
||||||
sidebar-chatYesterday = Gestern
|
sidebar-chatYesterday = Gestern
|
||||||
sidebar-relativeDateMonthDay = { $day }. { $month }
|
sidebar-relativeDateMonthDay = { $day }. { $month }
|
||||||
modal-confirmDelete-title = Löschen bestätigen
|
modal-confirmDelete-title = Löschen bestätigen
|
||||||
@@ -464,6 +465,9 @@ editor-createdAt = Erstellt
|
|||||||
editor-updatedAt = Aktualisiert
|
editor-updatedAt = Aktualisiert
|
||||||
editor-publishedAt = Veröffentlicht
|
editor-publishedAt = Veröffentlicht
|
||||||
tags-noTags = Noch keine Tags
|
tags-noTags = Noch keine Tags
|
||||||
|
categories-section = Kategorien
|
||||||
|
categories-title = Titel ({ $language })
|
||||||
|
categories-actions = Aktionen
|
||||||
tags-cloudSection = Wolke
|
tags-cloudSection = Wolke
|
||||||
tags-cloudHelp = Wählen Sie einen oder mehrere Tags aus, um sie zu bearbeiten, zu löschen oder zusammenzuführen.
|
tags-cloudHelp = Wählen Sie einen oder mehrere Tags aus, um sie zu bearbeiten, zu löschen oder zusammenzuführen.
|
||||||
tags-selectedCount = { $count } ausgewählt
|
tags-selectedCount = { $count } ausgewählt
|
||||||
|
|||||||
@@ -330,6 +330,7 @@ tags-nav-cloud = Tag Cloud
|
|||||||
tags-nav-manage = Manage Tags
|
tags-nav-manage = Manage Tags
|
||||||
tags-nav-merge = Merge Tags
|
tags-nav-merge = Merge Tags
|
||||||
tags-nav-discover = Discover Tags
|
tags-nav-discover = Discover Tags
|
||||||
|
tags-nav-categories = Categories
|
||||||
sidebar-chatYesterday = Yesterday
|
sidebar-chatYesterday = Yesterday
|
||||||
sidebar-relativeDateMonthDay = { $month } { $day }
|
sidebar-relativeDateMonthDay = { $month } { $day }
|
||||||
modal-confirmDelete-title = Confirm Delete
|
modal-confirmDelete-title = Confirm Delete
|
||||||
@@ -464,6 +465,9 @@ editor-imageDropFailed = Failed to import { $path }: { $error }
|
|||||||
editor-imageDropEnrichment = Enrich dropped image: { $name }
|
editor-imageDropEnrichment = Enrich dropped image: { $name }
|
||||||
editor-imageDropEnrichmentFailed = Failed to enrich { $path }: { $error }
|
editor-imageDropEnrichmentFailed = Failed to enrich { $path }: { $error }
|
||||||
tags-noTags = No tags yet
|
tags-noTags = No tags yet
|
||||||
|
categories-section = Categories
|
||||||
|
categories-title = Title ({ $language })
|
||||||
|
categories-actions = Actions
|
||||||
tags-cloudSection = Cloud
|
tags-cloudSection = Cloud
|
||||||
tags-cloudHelp = Select one or more tags to edit, delete, or merge them.
|
tags-cloudHelp = Select one or more tags to edit, delete, or merge them.
|
||||||
tags-selectedCount = { $count } selected
|
tags-selectedCount = { $count } selected
|
||||||
|
|||||||
@@ -330,6 +330,7 @@ tags-nav-cloud = Nube de etiquetas
|
|||||||
tags-nav-manage = Gestionar etiquetas
|
tags-nav-manage = Gestionar etiquetas
|
||||||
tags-nav-merge = Fusionar etiquetas
|
tags-nav-merge = Fusionar etiquetas
|
||||||
tags-nav-discover = Descubrir etiquetas
|
tags-nav-discover = Descubrir etiquetas
|
||||||
|
tags-nav-categories = Categorías
|
||||||
sidebar-chatYesterday = Ayer
|
sidebar-chatYesterday = Ayer
|
||||||
sidebar-relativeDateMonthDay = { $day } { $month }
|
sidebar-relativeDateMonthDay = { $day } { $month }
|
||||||
modal-confirmDelete-title = Confirmar eliminación
|
modal-confirmDelete-title = Confirmar eliminación
|
||||||
@@ -464,6 +465,9 @@ editor-createdAt = Creado
|
|||||||
editor-updatedAt = Actualizado
|
editor-updatedAt = Actualizado
|
||||||
editor-publishedAt = Publicado
|
editor-publishedAt = Publicado
|
||||||
tags-noTags = Sin etiquetas
|
tags-noTags = Sin etiquetas
|
||||||
|
categories-section = Categorías
|
||||||
|
categories-title = Título ({ $language })
|
||||||
|
categories-actions = Acciones
|
||||||
tags-cloudSection = Nube
|
tags-cloudSection = Nube
|
||||||
tags-cloudHelp = Selecciona una o más etiquetas para editarlas, eliminarlas o fusionarlas.
|
tags-cloudHelp = Selecciona una o más etiquetas para editarlas, eliminarlas o fusionarlas.
|
||||||
tags-selectedCount = { $count } seleccionadas
|
tags-selectedCount = { $count } seleccionadas
|
||||||
|
|||||||
@@ -330,6 +330,7 @@ tags-nav-cloud = Nuage de tags
|
|||||||
tags-nav-manage = Gérer les tags
|
tags-nav-manage = Gérer les tags
|
||||||
tags-nav-merge = Fusionner les tags
|
tags-nav-merge = Fusionner les tags
|
||||||
tags-nav-discover = Découvrir les tags
|
tags-nav-discover = Découvrir les tags
|
||||||
|
tags-nav-categories = Catégories
|
||||||
sidebar-chatYesterday = Hier
|
sidebar-chatYesterday = Hier
|
||||||
sidebar-relativeDateMonthDay = { $day } { $month }
|
sidebar-relativeDateMonthDay = { $day } { $month }
|
||||||
modal-confirmDelete-title = Confirmer la suppression
|
modal-confirmDelete-title = Confirmer la suppression
|
||||||
@@ -464,6 +465,9 @@ editor-createdAt = Créé
|
|||||||
editor-updatedAt = Mis à jour
|
editor-updatedAt = Mis à jour
|
||||||
editor-publishedAt = Publié
|
editor-publishedAt = Publié
|
||||||
tags-noTags = Aucun tag
|
tags-noTags = Aucun tag
|
||||||
|
categories-section = Catégories
|
||||||
|
categories-title = Titre ({ $language })
|
||||||
|
categories-actions = Actions
|
||||||
tags-cloudSection = Nuage
|
tags-cloudSection = Nuage
|
||||||
tags-cloudHelp = Sélectionnez un ou plusieurs tags pour les modifier, les supprimer ou les fusionner.
|
tags-cloudHelp = Sélectionnez un ou plusieurs tags pour les modifier, les supprimer ou les fusionner.
|
||||||
tags-selectedCount = { $count } sélectionnés
|
tags-selectedCount = { $count } sélectionnés
|
||||||
|
|||||||
@@ -330,6 +330,7 @@ tags-nav-cloud = Nuvola di tag
|
|||||||
tags-nav-manage = Gestisci tag
|
tags-nav-manage = Gestisci tag
|
||||||
tags-nav-merge = Unisci tag
|
tags-nav-merge = Unisci tag
|
||||||
tags-nav-discover = Scopri tag
|
tags-nav-discover = Scopri tag
|
||||||
|
tags-nav-categories = Categorie
|
||||||
sidebar-chatYesterday = Ieri
|
sidebar-chatYesterday = Ieri
|
||||||
sidebar-relativeDateMonthDay = { $day } { $month }
|
sidebar-relativeDateMonthDay = { $day } { $month }
|
||||||
modal-confirmDelete-title = Conferma eliminazione
|
modal-confirmDelete-title = Conferma eliminazione
|
||||||
@@ -464,6 +465,9 @@ editor-createdAt = Creato
|
|||||||
editor-updatedAt = Aggiornato
|
editor-updatedAt = Aggiornato
|
||||||
editor-publishedAt = Pubblicato
|
editor-publishedAt = Pubblicato
|
||||||
tags-noTags = Nessun tag
|
tags-noTags = Nessun tag
|
||||||
|
categories-section = Categorie
|
||||||
|
categories-title = Titolo ({ $language })
|
||||||
|
categories-actions = Azioni
|
||||||
tags-cloudSection = Nuvola
|
tags-cloudSection = Nuvola
|
||||||
tags-cloudHelp = Seleziona uno o più tag per modificarli, eliminarli o unirli.
|
tags-cloudHelp = Seleziona uno o più tag per modificarli, eliminarli o unirli.
|
||||||
tags-selectedCount = { $count } selezionati
|
tags-selectedCount = { $count } selezionati
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ value SettingsView {
|
|||||||
active_sections: List<String> -- visible sections after search filter
|
active_sections: List<String> -- visible sections after search filter
|
||||||
project_section: SettingsProjectSection?
|
project_section: SettingsProjectSection?
|
||||||
editor_section: SettingsEditorSection?
|
editor_section: SettingsEditorSection?
|
||||||
categories: List<SettingsCategoryRow>
|
|
||||||
ai_section: SettingsAISection?
|
ai_section: SettingsAISection?
|
||||||
technology_section: SettingsTechnologySection?
|
technology_section: SettingsTechnologySection?
|
||||||
publishing_section: SettingsPublishingSection?
|
publishing_section: SettingsPublishingSection?
|
||||||
@@ -42,16 +41,6 @@ value SettingsEditorSection {
|
|||||||
hide_unchanged_regions: Boolean -- checkbox
|
hide_unchanged_regions: Boolean -- checkbox
|
||||||
}
|
}
|
||||||
|
|
||||||
value SettingsCategoryRow {
|
|
||||||
name: String -- read-only for protected categories
|
|
||||||
title: String -- editable
|
|
||||||
render_in_lists: Boolean -- checkbox
|
|
||||||
show_titles: Boolean -- checkbox
|
|
||||||
post_template_slug: String? -- select
|
|
||||||
list_template_slug: String? -- select
|
|
||||||
is_protected: Boolean -- true for: article, aside, page, picture
|
|
||||||
}
|
|
||||||
|
|
||||||
value SettingsAISection {
|
value SettingsAISection {
|
||||||
online: SettingsAIEndpoint
|
online: SettingsAIEndpoint
|
||||||
airplane: SettingsAIEndpoint
|
airplane: SettingsAIEndpoint
|
||||||
@@ -97,11 +86,6 @@ value SettingsDataSection {
|
|||||||
rebuild_targets: List<String> -- posts | media | scripts | templates | links | thumbnails | embedding
|
rebuild_targets: List<String> -- posts | media | scripts | templates | links | thumbnails | embedding
|
||||||
}
|
}
|
||||||
|
|
||||||
invariant SettingsProtectedCategories {
|
|
||||||
-- Protected categories (article, aside, page, picture) cannot be deleted.
|
|
||||||
-- Their Remove button is disabled.
|
|
||||||
}
|
|
||||||
|
|
||||||
invariant SettingsMCPAgents {
|
invariant SettingsMCPAgents {
|
||||||
-- MCP section has exactly 7 agent rows in this order:
|
-- MCP section has exactly 7 agent rows in this order:
|
||||||
-- Claude Code, Claude Desktop, GitHub Copilot, Gemini CLI,
|
-- Claude Code, Claude Desktop, GitHub Copilot, Gemini CLI,
|
||||||
|
|||||||
@@ -314,6 +314,8 @@ value CategoryMetaJson {
|
|||||||
value CategorySettings {
|
value CategorySettings {
|
||||||
renderInLists: Boolean
|
renderInLists: Boolean
|
||||||
showTitle: Boolean
|
showTitle: Boolean
|
||||||
|
title: String?
|
||||||
|
titles: Map<String, String>
|
||||||
postTemplateSlug: String?
|
postTemplateSlug: String?
|
||||||
listTemplateSlug: String?
|
listTemplateSlug: String?
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ surface PublishingPreferencesSurface {
|
|||||||
|
|
||||||
value CategoryRenderSettings {
|
value CategoryRenderSettings {
|
||||||
title: String?
|
title: String?
|
||||||
|
titles: Map<String, String> -- translated titles keyed by language
|
||||||
render_in_lists: Boolean
|
render_in_lists: Boolean
|
||||||
show_title: Boolean
|
show_title: Boolean
|
||||||
post_template_slug: String?
|
post_template_slug: String?
|
||||||
@@ -78,7 +79,9 @@ invariant MetadataPersistedAsFiles {
|
|||||||
-- sshMode, sshRemotePath, sshUser (nil keys absent; sshMode defaults to scp).
|
-- sshMode, sshRemotePath, sshUser (nil keys absent; sshMode defaults to scp).
|
||||||
-- Category names and per-category keys are byte-order sorted; per-category
|
-- Category names and per-category keys are byte-order sorted; per-category
|
||||||
-- keys are listTemplateSlug, postTemplateSlug, renderInLists, showTitle,
|
-- keys are listTemplateSlug, postTemplateSlug, renderInLists, showTitle,
|
||||||
-- title. categories.json uses case-sensitive byte-order sorting. tags.json
|
-- title, titles. The title is for the main language; titles contains
|
||||||
|
-- configured translations keyed by language. categories.json uses
|
||||||
|
-- case-sensitive byte-order sorting. tags.json
|
||||||
-- remains case-insensitively name-sorted; entry keys are color, name,
|
-- remains case-insensitively name-sorted; entry keys are color, name,
|
||||||
-- postTemplateSlug and blank optional values are absent.
|
-- postTemplateSlug and blank optional values are absent.
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -222,6 +222,13 @@ rule BuildListAssigns {
|
|||||||
ensures: ListRenderAssigns
|
ensures: ListRenderAssigns
|
||||||
}
|
}
|
||||||
|
|
||||||
|
invariant CategoryArchiveTitleLanguage {
|
||||||
|
-- A category archive uses the title configured for its render language.
|
||||||
|
-- The main-language archive uses title; translated archives use the entry
|
||||||
|
-- in titles keyed by their language; a missing title falls back to the
|
||||||
|
-- category name.
|
||||||
|
}
|
||||||
|
|
||||||
invariant SharedRenderPathForPreviewAndGeneration {
|
invariant SharedRenderPathForPreviewAndGeneration {
|
||||||
-- Preview and generation produce identical HTML for the same input because
|
-- Preview and generation produce identical HTML for the same input because
|
||||||
-- both build assigns through this subsystem and render via the same Liquid
|
-- both build assigns through this subsystem and render via the same Liquid
|
||||||
|
|||||||
@@ -447,16 +447,15 @@ value SettingsNavEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
invariant SettingsNavSections {
|
invariant SettingsNavSections {
|
||||||
-- Settings navigation has exactly 9 entries in this fixed order:
|
-- Settings navigation has exactly 8 entries in this fixed order:
|
||||||
-- 1. section="project", icon="folder", label_key="settings.nav.project"
|
-- 1. section="project", icon="folder", label_key="settings.nav.project"
|
||||||
-- 2. section="editor", icon="notepad", label_key="settings.nav.editor"
|
-- 2. section="editor", icon="notepad", label_key="settings.nav.editor"
|
||||||
-- 3. section="content", icon="clipboard", label_key="settings.nav.content"
|
-- 3. section="ai", icon="robot", label_key="settings.nav.ai"
|
||||||
-- 4. section="ai", icon="robot", label_key="settings.nav.ai"
|
-- 4. section="technology", icon="gear", label_key="settings.nav.technology"
|
||||||
-- 5. section="technology", icon="gear", label_key="settings.nav.technology"
|
-- 5. section="publishing", icon="rocket", label_key="settings.nav.publishing"
|
||||||
-- 6. section="publishing", icon="rocket", label_key="settings.nav.publishing"
|
-- 6. section="data", icon="database", label_key="settings.nav.data"
|
||||||
-- 7. section="data", icon="database", label_key="settings.nav.data"
|
-- 7. section="mcp", icon="plug", label_key="settings.nav.mcp"
|
||||||
-- 8. section="mcp", icon="plug", label_key="settings.nav.mcp"
|
-- 8. section="style", icon="palette", label_key="settings.nav.style"
|
||||||
-- 9. section="style", icon="palette", label_key="settings.nav.style"
|
|
||||||
-- Labels are localised via their label_key through i18n.
|
-- Labels are localised via their label_key through i18n.
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -513,10 +512,11 @@ value TagsNavEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
invariant TagsNavSections {
|
invariant TagsNavSections {
|
||||||
-- Tags navigation has exactly 3 entries in this fixed order:
|
-- Tags navigation has exactly 4 entries in this fixed order:
|
||||||
-- 1. section="cloud", icon="cloud", label_key="tags.nav.cloud" -- tag cloud visualisation
|
-- 1. section="cloud", icon="cloud", label_key="tags.nav.cloud" -- tag cloud visualisation
|
||||||
-- 2. section="manage", icon="pencil", label_key="tags.nav.manage" -- create/edit tags
|
-- 2. section="manage", icon="pencil", label_key="tags.nav.manage" -- create/edit tags
|
||||||
-- 3. section="merge", icon="merge", label_key="tags.nav.merge" -- merge duplicate tags
|
-- 3. section="merge", icon="merge", label_key="tags.nav.merge" -- merge duplicate tags
|
||||||
|
-- 4. section="categories", icon="list", label_key="tags.nav.categories" -- edit categories
|
||||||
-- Labels are localised via their label_key through i18n.
|
-- Labels are localised via their label_key through i18n.
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -559,6 +559,39 @@ rule TagsNavClick {
|
|||||||
-- Active section is persisted across sidebar switches
|
-- Active section is persisted across sidebar switches
|
||||||
}
|
}
|
||||||
|
|
||||||
|
value CategoryEditor {
|
||||||
|
main_language: String
|
||||||
|
translation_languages: List<String>
|
||||||
|
rows: List<CategoryEditorRow>
|
||||||
|
}
|
||||||
|
|
||||||
|
value CategoryEditorRow {
|
||||||
|
name: String
|
||||||
|
titles: Map<String, String>
|
||||||
|
render_in_lists: Boolean
|
||||||
|
show_title: Boolean
|
||||||
|
post_template_slug: String?
|
||||||
|
list_template_slug: String?
|
||||||
|
is_protected: Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
surface CategoryEditorSurface {
|
||||||
|
context editor: CategoryEditor
|
||||||
|
|
||||||
|
exposes:
|
||||||
|
editor.main_language
|
||||||
|
editor.translation_languages
|
||||||
|
editor.rows.count
|
||||||
|
|
||||||
|
@guarantee TableLayout
|
||||||
|
-- Category settings render as one table. Its first title column is the
|
||||||
|
-- main language and every configured translation language adds one
|
||||||
|
-- further title column.
|
||||||
|
|
||||||
|
@guarantee ProtectedCategories
|
||||||
|
-- article, aside, page, and picture cannot be removed.
|
||||||
|
}
|
||||||
|
|
||||||
-- ─── 8. Chat view ─────────────────────────────────────────────
|
-- ─── 8. Chat view ─────────────────────────────────────────────
|
||||||
|
|
||||||
-- Follows SidebarEntityListPattern.
|
-- Follows SidebarEntityListPattern.
|
||||||
|
|||||||
Reference in New Issue
Block a user