delete pages/spaces
This commit is contained in:
128
src/api.rs
128
src/api.rs
@@ -6,6 +6,14 @@ use tracing::info;
|
|||||||
|
|
||||||
use crate::app::{ApiList, LoginRequest, Page, SearchResult, SidebarPagesRequest, Space};
|
use crate::app::{ApiList, LoginRequest, Page, SearchResult, SidebarPagesRequest, Space};
|
||||||
|
|
||||||
|
/// Convert a display name into a slug accepted by Docmost: lowercase, letters and digits only.
|
||||||
|
pub fn slugify(name: &str) -> String {
|
||||||
|
name.to_lowercase()
|
||||||
|
.chars()
|
||||||
|
.filter(|c| c.is_alphanumeric())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn auth_client() -> Result<Client, String> {
|
pub fn auth_client() -> Result<Client, String> {
|
||||||
Client::builder()
|
Client::builder()
|
||||||
.timeout(Duration::from_secs(10))
|
.timeout(Duration::from_secs(10))
|
||||||
@@ -315,3 +323,123 @@ pub async fn create_page(
|
|||||||
|
|
||||||
Ok((id, returned_title))
|
Ok((id, returned_title))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct CreateSpaceRequest<'a> {
|
||||||
|
name: &'a str,
|
||||||
|
slug: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_space(
|
||||||
|
base_url: &str,
|
||||||
|
token: &str,
|
||||||
|
name: &str,
|
||||||
|
) -> Result<Space, String> {
|
||||||
|
let client = auth_client()?;
|
||||||
|
let slug = slugify(name);
|
||||||
|
|
||||||
|
info!("create_space: name={name:?} slug={slug:?}");
|
||||||
|
|
||||||
|
let resp = client
|
||||||
|
.post(format!("{base_url}/spaces/create"))
|
||||||
|
.header("Cookie", format!("authToken={token}"))
|
||||||
|
.json(&CreateSpaceRequest { name, slug })
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Connection error: {e}"))?;
|
||||||
|
|
||||||
|
let status = resp.status();
|
||||||
|
let body = resp.text().await.unwrap_or_default();
|
||||||
|
|
||||||
|
info!("create_space: status={status} body={body}");
|
||||||
|
|
||||||
|
if !status.is_success() {
|
||||||
|
if let Ok(v) = serde_json::from_str::<serde_json::Value>(&body) {
|
||||||
|
if let Some(msg) = v.get("message").and_then(|m| m.as_str()) {
|
||||||
|
return Err(msg.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Err(format!("Create space error {status}: {body}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let v: serde_json::Value =
|
||||||
|
serde_json::from_str(&body).map_err(|e| format!("Parse error: {e}"))?;
|
||||||
|
|
||||||
|
let space_val = v.get("data").unwrap_or(&v);
|
||||||
|
let space: Space =
|
||||||
|
serde_json::from_value(space_val.clone()).map_err(|e| format!("Parse space error: {e}"))?;
|
||||||
|
|
||||||
|
Ok(space)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct DeletePageRequest<'a> {
|
||||||
|
#[serde(rename = "pageId")]
|
||||||
|
page_id: &'a str,
|
||||||
|
#[serde(rename = "permanentlyDelete")]
|
||||||
|
permanently_delete: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_page(base_url: &str, token: &str, page_id: &str) -> Result<(), String> {
|
||||||
|
let client = auth_client()?;
|
||||||
|
|
||||||
|
info!("delete_page: page_id={page_id}");
|
||||||
|
|
||||||
|
let resp = client
|
||||||
|
.post(format!("{base_url}/pages/delete"))
|
||||||
|
.header("Cookie", format!("authToken={token}"))
|
||||||
|
.json(&DeletePageRequest { page_id, permanently_delete: false })
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Connection error: {e}"))?;
|
||||||
|
|
||||||
|
let status = resp.status();
|
||||||
|
info!("delete_page: status={status}");
|
||||||
|
|
||||||
|
if !status.is_success() {
|
||||||
|
let body = resp.text().await.unwrap_or_default();
|
||||||
|
if let Ok(v) = serde_json::from_str::<serde_json::Value>(&body) {
|
||||||
|
if let Some(msg) = v.get("message").and_then(|m| m.as_str()) {
|
||||||
|
return Err(msg.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Err(format!("Delete page error {status}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct DeleteSpaceRequest<'a> {
|
||||||
|
#[serde(rename = "spaceId")]
|
||||||
|
space_id: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_space(base_url: &str, token: &str, space_id: &str) -> Result<(), String> {
|
||||||
|
let client = auth_client()?;
|
||||||
|
|
||||||
|
info!("delete_space: space_id={space_id}");
|
||||||
|
|
||||||
|
let resp = client
|
||||||
|
.post(format!("{base_url}/spaces/delete"))
|
||||||
|
.header("Cookie", format!("authToken={token}"))
|
||||||
|
.json(&DeleteSpaceRequest { space_id })
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Connection error: {e}"))?;
|
||||||
|
|
||||||
|
let status = resp.status();
|
||||||
|
info!("delete_space: status={status}");
|
||||||
|
|
||||||
|
if !status.is_success() {
|
||||||
|
let body = resp.text().await.unwrap_or_default();
|
||||||
|
if let Ok(v) = serde_json::from_str::<serde_json::Value>(&body) {
|
||||||
|
if let Some(msg) = v.get("message").and_then(|m| m.as_str()) {
|
||||||
|
return Err(msg.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Err(format!("Delete space error {status}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
54
src/app.rs
54
src/app.rs
@@ -128,6 +128,51 @@ impl NewPageDialog {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct NewSpaceDialog {
|
||||||
|
pub name: String,
|
||||||
|
pub creating: bool,
|
||||||
|
pub error: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NewSpaceDialog {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self { name: String::new(), creating: false, error: None }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum DeleteTarget {
|
||||||
|
Space { id: String, name: String },
|
||||||
|
Page { id: String, title: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DeleteTarget {
|
||||||
|
pub fn display_name(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
DeleteTarget::Space { name, .. } => name,
|
||||||
|
DeleteTarget::Page { title, .. } => title,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn kind(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
DeleteTarget::Space { .. } => "space",
|
||||||
|
DeleteTarget::Page { .. } => "page",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DeleteConfirmDialog {
|
||||||
|
pub target: DeleteTarget,
|
||||||
|
pub deleting: bool,
|
||||||
|
pub error: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DeleteConfirmDialog {
|
||||||
|
pub fn new(target: DeleteTarget) -> Self {
|
||||||
|
Self { target, deleting: false, error: None }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct MainView {
|
pub struct MainView {
|
||||||
pub spaces: Vec<Space>,
|
pub spaces: Vec<Space>,
|
||||||
pub pages: Vec<Page>,
|
pub pages: Vec<Page>,
|
||||||
@@ -139,6 +184,8 @@ pub struct MainView {
|
|||||||
pub loading_page_content: bool,
|
pub loading_page_content: bool,
|
||||||
pub error: Option<String>,
|
pub error: Option<String>,
|
||||||
pub new_page_dialog: Option<NewPageDialog>,
|
pub new_page_dialog: Option<NewPageDialog>,
|
||||||
|
pub new_space_dialog: Option<NewSpaceDialog>,
|
||||||
|
pub delete_dialog: Option<DeleteConfirmDialog>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MainView {
|
impl MainView {
|
||||||
@@ -154,6 +201,8 @@ impl MainView {
|
|||||||
loading_page_content: false,
|
loading_page_content: false,
|
||||||
error: None,
|
error: None,
|
||||||
new_page_dialog: None,
|
new_page_dialog: None,
|
||||||
|
new_space_dialog: None,
|
||||||
|
delete_dialog: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -267,6 +316,11 @@ pub enum AppMsg {
|
|||||||
SaveError(String),
|
SaveError(String),
|
||||||
PageCreated { page_id: String, title: String },
|
PageCreated { page_id: String, title: String },
|
||||||
CreateError(String),
|
CreateError(String),
|
||||||
|
SpaceCreated(Space),
|
||||||
|
CreateSpaceError(String),
|
||||||
|
SpaceDeleted(String),
|
||||||
|
PageDeleted(String),
|
||||||
|
DeleteError(String),
|
||||||
SearchResults(Vec<SearchResult>),
|
SearchResults(Vec<SearchResult>),
|
||||||
SearchError(String),
|
SearchError(String),
|
||||||
ApiError(String),
|
ApiError(String),
|
||||||
|
|||||||
136
src/main.rs
136
src/main.rs
@@ -16,8 +16,8 @@ use tui_textarea::TextArea;
|
|||||||
use tracing::info;
|
use tracing::info;
|
||||||
use tracing_subscriber::fmt;
|
use tracing_subscriber::fmt;
|
||||||
|
|
||||||
use api::{create_page, do_login, fetch_page_content, fetch_pages, fetch_spaces, save_page, search_pages};
|
use api::{create_page, create_space, delete_page, delete_space, do_login, fetch_page_content, fetch_pages, fetch_spaces, save_page, search_pages};
|
||||||
use app::{App, AppMsg, AppState, EditorFocus, EditorStatus, EditorView, LoginField, NewPageDialog, Panel, SearchView};
|
use app::{App, AppMsg, AppState, DeleteConfirmDialog, DeleteTarget, EditorFocus, EditorStatus, EditorView, LoginField, NewPageDialog, NewSpaceDialog, Panel, SearchView};
|
||||||
use ui::{draw_editor, draw_login, draw_main, draw_search};
|
use ui::{draw_editor, draw_login, draw_main, draw_search};
|
||||||
|
|
||||||
// ─── Entry point ──────────────────────────────────────────────────────────────
|
// ─── Entry point ──────────────────────────────────────────────────────────────
|
||||||
@@ -168,6 +168,48 @@ async fn run_app(terminal: &mut Terminal<CrosstermBackend<io::Stdout>>) -> io::R
|
|||||||
d.error = Some(e);
|
d.error = Some(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
AppMsg::SpaceDeleted(space_id) => {
|
||||||
|
info!("space deleted: {space_id}");
|
||||||
|
app.main.delete_dialog = None;
|
||||||
|
app.main.spaces.retain(|s| s.id != space_id);
|
||||||
|
app.main.selected_space = 0;
|
||||||
|
app.main.pages = vec![];
|
||||||
|
if let Some(space) = app.main.spaces.first() {
|
||||||
|
app.main.loading_pages = true;
|
||||||
|
spawn_fetch_pages_msg(&app.base_url, &app.token, &space.id, &tx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AppMsg::PageDeleted(page_id) => {
|
||||||
|
info!("page deleted: {page_id}");
|
||||||
|
app.main.delete_dialog = None;
|
||||||
|
app.main.pages.retain(|p| p.id != page_id);
|
||||||
|
if app.main.selected_page >= app.main.pages.len() {
|
||||||
|
app.main.selected_page = app.main.pages.len().saturating_sub(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AppMsg::DeleteError(e) => {
|
||||||
|
info!("delete error: {e}");
|
||||||
|
if let Some(d) = app.main.delete_dialog.as_mut() {
|
||||||
|
d.deleting = false;
|
||||||
|
d.error = Some(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AppMsg::SpaceCreated(space) => {
|
||||||
|
info!("space created: id={} name={:?}", space.id, space.name);
|
||||||
|
app.main.new_space_dialog = None;
|
||||||
|
// Add to list and select it
|
||||||
|
app.main.spaces.push(space);
|
||||||
|
app.main.selected_space = app.main.spaces.len() - 1;
|
||||||
|
app.main.pages = vec![];
|
||||||
|
app.main.loading_pages = false;
|
||||||
|
}
|
||||||
|
AppMsg::CreateSpaceError(e) => {
|
||||||
|
info!("create space error: {e}");
|
||||||
|
if let Some(d) = app.main.new_space_dialog.as_mut() {
|
||||||
|
d.creating = false;
|
||||||
|
d.error = Some(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
AppMsg::SearchResults(results) => {
|
AppMsg::SearchResults(results) => {
|
||||||
info!("search results: {} items", results.len());
|
info!("search results: {} items", results.len());
|
||||||
app.search.loading = false;
|
app.search.loading = false;
|
||||||
@@ -280,6 +322,75 @@ async fn run_app(terminal: &mut Terminal<CrosstermBackend<io::Stdout>>) -> io::R
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Delete confirm dialog intercepts all keys when open ──
|
||||||
|
if let Some(dialog) = app.main.delete_dialog.as_mut() {
|
||||||
|
if dialog.deleting { continue; }
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Esc => { app.main.delete_dialog = None; }
|
||||||
|
KeyCode::Char('d') => {
|
||||||
|
dialog.deleting = true;
|
||||||
|
dialog.error = None;
|
||||||
|
let base_url = app.base_url.clone();
|
||||||
|
let token = app.token.clone();
|
||||||
|
let target = dialog.target.clone();
|
||||||
|
let tx2 = tx.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let msg = match &target {
|
||||||
|
DeleteTarget::Space { id, .. } => {
|
||||||
|
match delete_space(&base_url, &token, id).await {
|
||||||
|
Ok(()) => AppMsg::SpaceDeleted(id.clone()),
|
||||||
|
Err(e) => AppMsg::DeleteError(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DeleteTarget::Page { id, .. } => {
|
||||||
|
match delete_page(&base_url, &token, id).await {
|
||||||
|
Ok(()) => AppMsg::PageDeleted(id.clone()),
|
||||||
|
Err(e) => AppMsg::DeleteError(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let _ = tx2.send(msg);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── New-space dialog intercepts all keys when open ──
|
||||||
|
if let Some(dialog) = app.main.new_space_dialog.as_mut() {
|
||||||
|
if dialog.creating { continue; }
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Esc => { app.main.new_space_dialog = None; }
|
||||||
|
KeyCode::Backspace => { dialog.name.pop(); }
|
||||||
|
KeyCode::Enter => {
|
||||||
|
if dialog.name.trim().is_empty() {
|
||||||
|
dialog.error = Some("Name cannot be empty.".into());
|
||||||
|
} else {
|
||||||
|
dialog.creating = true;
|
||||||
|
dialog.error = None;
|
||||||
|
let base_url = app.base_url.clone();
|
||||||
|
let token = app.token.clone();
|
||||||
|
let name = dialog.name.trim().to_string();
|
||||||
|
let tx2 = tx.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let msg = match create_space(&base_url, &token, &name).await {
|
||||||
|
Ok(space) => AppMsg::SpaceCreated(space),
|
||||||
|
Err(e) => AppMsg::CreateSpaceError(e),
|
||||||
|
};
|
||||||
|
let _ = tx2.send(msg);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Char(c) if !key.modifiers.contains(KeyModifiers::CONTROL) => {
|
||||||
|
dialog.error = None;
|
||||||
|
dialog.name.push(c);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// ── New-page dialog intercepts all keys when open ──
|
// ── New-page dialog intercepts all keys when open ──
|
||||||
if let Some(dialog) = app.main.new_page_dialog.as_mut() {
|
if let Some(dialog) = app.main.new_page_dialog.as_mut() {
|
||||||
if dialog.creating { continue; }
|
if dialog.creating { continue; }
|
||||||
@@ -355,6 +466,27 @@ async fn run_app(terminal: &mut Terminal<CrosstermBackend<io::Stdout>>) -> io::R
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// d → delete focused item
|
||||||
|
KeyCode::Char('d') => {
|
||||||
|
let target = match app.main.focus {
|
||||||
|
Panel::Spaces => app.main.spaces
|
||||||
|
.get(app.main.selected_space)
|
||||||
|
.map(|s| DeleteTarget::Space { id: s.id.clone(), name: s.name.clone() }),
|
||||||
|
Panel::Pages => app.main.pages
|
||||||
|
.get(app.main.selected_page)
|
||||||
|
.map(|p| DeleteTarget::Page {
|
||||||
|
id: p.id.clone(),
|
||||||
|
title: p.title.clone().unwrap_or_else(|| "Untitled".into()),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
if let Some(target) = target {
|
||||||
|
app.main.delete_dialog = Some(DeleteConfirmDialog::new(target));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Ctrl+N → new space
|
||||||
|
KeyCode::Char('n') if key.modifiers.contains(KeyModifiers::CONTROL) => {
|
||||||
|
app.main.new_space_dialog = Some(NewSpaceDialog::new());
|
||||||
|
}
|
||||||
// n → new page in current space
|
// n → new page in current space
|
||||||
KeyCode::Char('n') => {
|
KeyCode::Char('n') => {
|
||||||
if !app.main.spaces.is_empty() {
|
if !app.main.spaces.is_empty() {
|
||||||
|
|||||||
146
src/ui.rs
146
src/ui.rs
@@ -8,8 +8,8 @@ use ratatui::{
|
|||||||
use tui_textarea::TextArea;
|
use tui_textarea::TextArea;
|
||||||
|
|
||||||
use crate::app::{
|
use crate::app::{
|
||||||
EditorFocus, EditorStatus, EditorView, LoginField, LoginForm, MainView, NewPageDialog, Panel,
|
DeleteConfirmDialog, EditorFocus, EditorStatus, EditorView, LoginField, LoginForm, MainView,
|
||||||
SearchView,
|
NewPageDialog, NewSpaceDialog, Panel, SearchView,
|
||||||
};
|
};
|
||||||
|
|
||||||
// ─── Login screen ─────────────────────────────────────────────────────────────
|
// ─── Login screen ─────────────────────────────────────────────────────────────
|
||||||
@@ -235,7 +235,7 @@ pub fn draw_main(f: &mut Frame, main: &MainView) {
|
|||||||
.and_then(|p| p.title.as_deref())
|
.and_then(|p| p.title.as_deref())
|
||||||
.unwrap_or("-");
|
.unwrap_or("-");
|
||||||
format!(
|
format!(
|
||||||
"Space: {space_name}\nPage: {page_title}\n\n_______________________\nEnter open editor\nTab switch panel\n↑↓/j k navigate\nq/Esc quit\n//Ctrl+F search"
|
"Space: {space_name}\nPage: {page_title}\n\n_______________________\nEnter open editor\nTab switch panel\n↑↓/j k navigate\nq/Esc quit\n//Ctrl+F search\n n +new page\n Ctrl+n New Space"
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -244,9 +244,13 @@ pub fn draw_main(f: &mut Frame, main: &MainView) {
|
|||||||
layout[1],
|
layout[1],
|
||||||
);
|
);
|
||||||
|
|
||||||
// ── New-page dialog (overlay) ──
|
// ── Dialogs (overlays, only one shown at a time) ──
|
||||||
if let Some(dialog) = &main.new_page_dialog {
|
if let Some(dialog) = &main.delete_dialog {
|
||||||
|
draw_delete_confirm_dialog(f, dialog, size);
|
||||||
|
} else if let Some(dialog) = &main.new_page_dialog {
|
||||||
draw_new_page_dialog(f, dialog, size);
|
draw_new_page_dialog(f, dialog, size);
|
||||||
|
} else if let Some(dialog) = &main.new_space_dialog {
|
||||||
|
draw_new_space_dialog(f, dialog, size);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,7 +258,11 @@ fn draw_new_page_dialog(f: &mut Frame, dialog: &NewPageDialog, size: Rect) {
|
|||||||
let area = centered_rect(50, 9, size);
|
let area = centered_rect(50, 9, size);
|
||||||
f.render_widget(Clear, area);
|
f.render_widget(Clear, area);
|
||||||
|
|
||||||
let title = if dialog.creating { " Creating… " } else { " New Page " };
|
let title = if dialog.creating {
|
||||||
|
" Creating… "
|
||||||
|
} else {
|
||||||
|
" New Page "
|
||||||
|
};
|
||||||
f.render_widget(
|
f.render_widget(
|
||||||
Block::default()
|
Block::default()
|
||||||
.title(title)
|
.title(title)
|
||||||
@@ -272,7 +280,11 @@ fn draw_new_page_dialog(f: &mut Frame, dialog: &NewPageDialog, size: Rect) {
|
|||||||
|
|
||||||
let rows = Layout::default()
|
let rows = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.constraints([Constraint::Length(3), Constraint::Length(1), Constraint::Length(2)])
|
.constraints([
|
||||||
|
Constraint::Length(3),
|
||||||
|
Constraint::Length(1),
|
||||||
|
Constraint::Length(2),
|
||||||
|
])
|
||||||
.split(inner);
|
.split(inner);
|
||||||
|
|
||||||
// Title input
|
// Title input
|
||||||
@@ -385,6 +397,126 @@ pub fn draw_editor(f: &mut Frame, editor: &EditorView, textarea: &mut TextArea<'
|
|||||||
f.render_widget(Paragraph::new(status_line), layout[2]);
|
f.render_widget(Paragraph::new(status_line), layout[2]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn draw_delete_confirm_dialog(f: &mut Frame, dialog: &DeleteConfirmDialog, size: Rect) {
|
||||||
|
let area = centered_rect(52, 9, size);
|
||||||
|
f.render_widget(Clear, area);
|
||||||
|
|
||||||
|
let kind = dialog.target.kind();
|
||||||
|
let name = dialog.target.display_name();
|
||||||
|
|
||||||
|
let title = if dialog.deleting {
|
||||||
|
format!(" Deleting {kind}… ")
|
||||||
|
} else {
|
||||||
|
format!(" Delete {kind} ")
|
||||||
|
};
|
||||||
|
|
||||||
|
f.render_widget(
|
||||||
|
Block::default()
|
||||||
|
.title(title)
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.style(Style::default().fg(Color::Red)),
|
||||||
|
area,
|
||||||
|
);
|
||||||
|
|
||||||
|
let inner = Rect {
|
||||||
|
x: area.x + 2,
|
||||||
|
y: area.y + 1,
|
||||||
|
width: area.width.saturating_sub(4),
|
||||||
|
height: area.height.saturating_sub(2),
|
||||||
|
};
|
||||||
|
|
||||||
|
let rows = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([Constraint::Length(2), Constraint::Length(1), Constraint::Length(2)])
|
||||||
|
.split(inner);
|
||||||
|
|
||||||
|
// Warning message
|
||||||
|
f.render_widget(
|
||||||
|
Paragraph::new(Line::from(vec![
|
||||||
|
Span::raw(" Move "),
|
||||||
|
Span::styled(name, Style::default().add_modifier(Modifier::BOLD)),
|
||||||
|
Span::raw(format!(" to trash?")),
|
||||||
|
])),
|
||||||
|
rows[0],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Error or confirmation hint
|
||||||
|
let hint = if let Some(err) = &dialog.error {
|
||||||
|
Paragraph::new(Span::styled(
|
||||||
|
format!(" {err}"),
|
||||||
|
Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Paragraph::new(Line::from(vec![
|
||||||
|
Span::styled(" d", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
|
||||||
|
Span::styled(": confirm ", Style::default().fg(Color::DarkGray)),
|
||||||
|
Span::styled("Esc", Style::default().add_modifier(Modifier::BOLD)),
|
||||||
|
Span::styled(": cancel", Style::default().fg(Color::DarkGray)),
|
||||||
|
]))
|
||||||
|
};
|
||||||
|
f.render_widget(hint, rows[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_new_space_dialog(f: &mut Frame, dialog: &NewSpaceDialog, size: Rect) {
|
||||||
|
let area = centered_rect(50, 9, size);
|
||||||
|
f.render_widget(Clear, area);
|
||||||
|
|
||||||
|
let title = if dialog.creating {
|
||||||
|
" Creating space… "
|
||||||
|
} else {
|
||||||
|
" New Space "
|
||||||
|
};
|
||||||
|
f.render_widget(
|
||||||
|
Block::default()
|
||||||
|
.title(title)
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.style(Style::default().fg(Color::Cyan)),
|
||||||
|
area,
|
||||||
|
);
|
||||||
|
|
||||||
|
let inner = Rect {
|
||||||
|
x: area.x + 2,
|
||||||
|
y: area.y + 1,
|
||||||
|
width: area.width.saturating_sub(4),
|
||||||
|
height: area.height.saturating_sub(2),
|
||||||
|
};
|
||||||
|
|
||||||
|
let rows = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([
|
||||||
|
Constraint::Length(3),
|
||||||
|
Constraint::Length(1),
|
||||||
|
Constraint::Length(2),
|
||||||
|
])
|
||||||
|
.split(inner);
|
||||||
|
|
||||||
|
// Name input + auto-slug preview
|
||||||
|
let slug_preview = crate::api::slugify(&dialog.name);
|
||||||
|
let input_content = format!("{}▌", dialog.name);
|
||||||
|
f.render_widget(
|
||||||
|
Paragraph::new(input_content).block(
|
||||||
|
Block::default()
|
||||||
|
.title(format!(" Name (slug: {slug_preview}) "))
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_style(Style::default().fg(Color::Cyan)),
|
||||||
|
),
|
||||||
|
rows[0],
|
||||||
|
);
|
||||||
|
|
||||||
|
let hint = if let Some(err) = &dialog.error {
|
||||||
|
Paragraph::new(Span::styled(
|
||||||
|
format!(" {err}"),
|
||||||
|
Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Paragraph::new(Span::styled(
|
||||||
|
" Enter: create · Esc: cancel",
|
||||||
|
Style::default().fg(Color::DarkGray),
|
||||||
|
))
|
||||||
|
};
|
||||||
|
f.render_widget(hint, rows[2]);
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Search screen ───────────────────────────────────────────────────────────
|
// ─── Search screen ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
pub fn draw_search(f: &mut Frame, search: &SearchView) {
|
pub fn draw_search(f: &mut Frame, search: &SearchView) {
|
||||||
|
|||||||
Reference in New Issue
Block a user