Add key handling for ConfigureDates (space toggle components) and DefineFormulas (n new, d delete, text input mode) wizard steps. Render date component toggles, formula list with input area, and sample formulas derived from detected measures. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
348 lines
13 KiB
Rust
348 lines
13 KiB
Rust
use ratatui::{
|
|
buffer::Buffer,
|
|
layout::Rect,
|
|
style::{Color, Modifier, Style},
|
|
widgets::{Block, Borders, Clear, Widget},
|
|
};
|
|
|
|
use crate::import::analyzer::{DateComponent, FieldKind};
|
|
use crate::import::wizard::{ImportWizard, WizardStep};
|
|
|
|
pub struct ImportWizardWidget<'a> {
|
|
pub wizard: &'a ImportWizard,
|
|
}
|
|
|
|
impl<'a> ImportWizardWidget<'a> {
|
|
pub fn new(wizard: &'a ImportWizard) -> Self {
|
|
Self { wizard }
|
|
}
|
|
}
|
|
|
|
impl<'a> Widget for ImportWizardWidget<'a> {
|
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
|
let popup_w = area.width.min(80);
|
|
let popup_h = area.height.min(30);
|
|
let x = area.x + area.width.saturating_sub(popup_w) / 2;
|
|
let y = area.y + area.height.saturating_sub(popup_h) / 2;
|
|
let popup_area = Rect::new(x, y, popup_w, popup_h);
|
|
|
|
Clear.render(popup_area, buf);
|
|
|
|
let title = match self.wizard.step {
|
|
WizardStep::Preview => " Import Wizard — Preview ",
|
|
WizardStep::SelectArrayPath => " Import Wizard — Select Array ",
|
|
WizardStep::ReviewProposals => " Import Wizard — Review Fields ",
|
|
WizardStep::ConfigureDates => " Import Wizard — Date Components ",
|
|
WizardStep::DefineFormulas => " Import Wizard — Formulas ",
|
|
WizardStep::NameModel => " Import Wizard — Name Model ",
|
|
WizardStep::Done => " Import Wizard — Done ",
|
|
};
|
|
|
|
let block = Block::default()
|
|
.borders(Borders::ALL)
|
|
.border_style(Style::default().fg(Color::Magenta))
|
|
.title(title);
|
|
let inner = block.inner(popup_area);
|
|
block.render(popup_area, buf);
|
|
|
|
let mut y = inner.y;
|
|
let x = inner.x;
|
|
let w = inner.width as usize;
|
|
|
|
match &self.wizard.step {
|
|
WizardStep::Preview => {
|
|
let summary = self.wizard.pipeline.preview_summary();
|
|
buf.set_string(x, y, truncate(&summary, w), Style::default());
|
|
y += 2;
|
|
buf.set_string(
|
|
x,
|
|
y,
|
|
"Press Enter to continue\u{2026}",
|
|
Style::default().fg(Color::Yellow),
|
|
);
|
|
}
|
|
WizardStep::SelectArrayPath => {
|
|
buf.set_string(
|
|
x,
|
|
y,
|
|
"Select the path containing records:",
|
|
Style::default().fg(Color::Yellow),
|
|
);
|
|
y += 1;
|
|
for (i, path) in self.wizard.pipeline.array_paths.iter().enumerate() {
|
|
if y >= inner.y + inner.height {
|
|
break;
|
|
}
|
|
let is_sel = i == self.wizard.cursor;
|
|
let style = if is_sel {
|
|
Style::default()
|
|
.fg(Color::Black)
|
|
.bg(Color::Magenta)
|
|
.add_modifier(Modifier::BOLD)
|
|
} else {
|
|
Style::default()
|
|
};
|
|
let label = format!(" {}", if path.is_empty() { "(root)" } else { path });
|
|
buf.set_string(x, y, truncate(&label, w), style);
|
|
y += 1;
|
|
}
|
|
y += 1;
|
|
buf.set_string(
|
|
x,
|
|
y,
|
|
"\u{2191}\u{2193} select Enter confirm",
|
|
Style::default().fg(Color::DarkGray),
|
|
);
|
|
}
|
|
WizardStep::ReviewProposals => {
|
|
buf.set_string(
|
|
x,
|
|
y,
|
|
"Review field proposals (Space toggle, c cycle kind):",
|
|
Style::default().fg(Color::Yellow),
|
|
);
|
|
y += 1;
|
|
let header = format!(" {:<20} {:<22} {:<6}", "Field", "Kind", "Accept");
|
|
buf.set_string(
|
|
x,
|
|
y,
|
|
truncate(&header, w),
|
|
Style::default()
|
|
.fg(Color::Gray)
|
|
.add_modifier(Modifier::UNDERLINED),
|
|
);
|
|
y += 1;
|
|
|
|
for (i, proposal) in self.wizard.pipeline.proposals.iter().enumerate() {
|
|
if y >= inner.y + inner.height - 2 {
|
|
break;
|
|
}
|
|
let is_sel = i == self.wizard.cursor;
|
|
|
|
let kind_color = match proposal.kind {
|
|
FieldKind::Category => Color::Green,
|
|
FieldKind::Measure => Color::Cyan,
|
|
FieldKind::TimeCategory => Color::Magenta,
|
|
FieldKind::Label => Color::DarkGray,
|
|
};
|
|
|
|
let accept_str = if proposal.accepted {
|
|
"[\u{2713}]"
|
|
} else {
|
|
"[ ]"
|
|
};
|
|
let row = format!(
|
|
" {:<20} {:<22} {}",
|
|
truncate(&proposal.field, 20),
|
|
truncate(proposal.kind_label(), 22),
|
|
accept_str
|
|
);
|
|
|
|
let style = if is_sel {
|
|
Style::default()
|
|
.fg(Color::Black)
|
|
.bg(Color::Cyan)
|
|
.add_modifier(Modifier::BOLD)
|
|
} else if proposal.accepted {
|
|
Style::default().fg(kind_color)
|
|
} else {
|
|
Style::default().fg(Color::DarkGray)
|
|
};
|
|
|
|
buf.set_string(x, y, truncate(&row, w), style);
|
|
y += 1;
|
|
}
|
|
let hint_y = inner.y + inner.height - 1;
|
|
buf.set_string(
|
|
x,
|
|
hint_y,
|
|
"Enter: next Space: toggle c: cycle kind Esc: cancel",
|
|
Style::default().fg(Color::DarkGray),
|
|
);
|
|
}
|
|
WizardStep::ConfigureDates => {
|
|
buf.set_string(
|
|
x,
|
|
y,
|
|
"Select date components to extract (Space toggle):",
|
|
Style::default().fg(Color::Yellow),
|
|
);
|
|
y += 1;
|
|
|
|
let tc_proposals = self.wizard.time_category_proposals();
|
|
let mut item_idx = 0;
|
|
for proposal in &tc_proposals {
|
|
if y >= inner.y + inner.height - 2 {
|
|
break;
|
|
}
|
|
let fmt_str = proposal.date_format.as_deref().unwrap_or("?");
|
|
let header = format!(" {} (format: {})", proposal.field, fmt_str);
|
|
buf.set_string(
|
|
x,
|
|
y,
|
|
truncate(&header, w),
|
|
Style::default()
|
|
.fg(Color::Magenta)
|
|
.add_modifier(Modifier::BOLD),
|
|
);
|
|
y += 1;
|
|
|
|
for component in &[
|
|
DateComponent::Year,
|
|
DateComponent::Month,
|
|
DateComponent::Quarter,
|
|
] {
|
|
if y >= inner.y + inner.height - 2 {
|
|
break;
|
|
}
|
|
let enabled = proposal.date_components.contains(component);
|
|
let check = if enabled { "[\u{2713}]" } else { "[ ]" };
|
|
let label = match component {
|
|
DateComponent::Year => "Year",
|
|
DateComponent::Month => "Month",
|
|
DateComponent::Quarter => "Quarter",
|
|
};
|
|
let row = format!(" {} {}", check, label);
|
|
let is_sel = item_idx == self.wizard.cursor;
|
|
let style = if is_sel {
|
|
Style::default()
|
|
.fg(Color::Black)
|
|
.bg(Color::Cyan)
|
|
.add_modifier(Modifier::BOLD)
|
|
} else if enabled {
|
|
Style::default().fg(Color::Green)
|
|
} else {
|
|
Style::default().fg(Color::DarkGray)
|
|
};
|
|
buf.set_string(x, y, truncate(&row, w), style);
|
|
y += 1;
|
|
item_idx += 1;
|
|
}
|
|
}
|
|
let hint_y = inner.y + inner.height - 1;
|
|
buf.set_string(
|
|
x,
|
|
hint_y,
|
|
"Space: toggle Enter: next Esc: cancel",
|
|
Style::default().fg(Color::DarkGray),
|
|
);
|
|
}
|
|
WizardStep::DefineFormulas => {
|
|
buf.set_string(
|
|
x,
|
|
y,
|
|
"Define formulas (optional):",
|
|
Style::default().fg(Color::Yellow),
|
|
);
|
|
y += 1;
|
|
|
|
// Show existing formulas
|
|
if self.wizard.pipeline.formulas.is_empty() && !self.wizard.formula_editing {
|
|
buf.set_string(
|
|
x,
|
|
y,
|
|
" (no formulas yet)",
|
|
Style::default().fg(Color::DarkGray),
|
|
);
|
|
y += 1;
|
|
}
|
|
for (i, formula) in self.wizard.pipeline.formulas.iter().enumerate() {
|
|
if y >= inner.y + inner.height - 5 {
|
|
break;
|
|
}
|
|
let is_sel = i == self.wizard.cursor && !self.wizard.formula_editing;
|
|
let style = if is_sel {
|
|
Style::default()
|
|
.fg(Color::Black)
|
|
.bg(Color::Cyan)
|
|
.add_modifier(Modifier::BOLD)
|
|
} else {
|
|
Style::default().fg(Color::Green)
|
|
};
|
|
buf.set_string(x, y, truncate(&format!(" {}", formula), w), style);
|
|
y += 1;
|
|
}
|
|
|
|
// Formula input area
|
|
if self.wizard.formula_editing {
|
|
y += 1;
|
|
buf.set_string(
|
|
x,
|
|
y,
|
|
"Formula (e.g., Profit = Revenue - Cost):",
|
|
Style::default().fg(Color::Yellow),
|
|
);
|
|
y += 1;
|
|
let input = format!("> {}\u{2588}", self.wizard.formula_buffer);
|
|
buf.set_string(x, y, truncate(&input, w), Style::default().fg(Color::Green));
|
|
y += 1;
|
|
}
|
|
|
|
// Sample formulas
|
|
let samples = self.wizard.sample_formulas();
|
|
if !samples.is_empty() {
|
|
y += 1;
|
|
buf.set_string(x, y, "Examples:", Style::default().fg(Color::DarkGray));
|
|
y += 1;
|
|
for sample in &samples {
|
|
if y >= inner.y + inner.height - 1 {
|
|
break;
|
|
}
|
|
buf.set_string(
|
|
x,
|
|
y,
|
|
truncate(&format!(" {}", sample), w),
|
|
Style::default().fg(Color::DarkGray),
|
|
);
|
|
y += 1;
|
|
}
|
|
}
|
|
|
|
let hint_y = inner.y + inner.height - 1;
|
|
let hint = if self.wizard.formula_editing {
|
|
"Enter: add Esc: cancel"
|
|
} else {
|
|
"n: new formula d: delete Enter: next Esc: cancel"
|
|
};
|
|
buf.set_string(x, hint_y, hint, Style::default().fg(Color::DarkGray));
|
|
}
|
|
WizardStep::NameModel => {
|
|
buf.set_string(x, y, "Model name:", Style::default().fg(Color::Yellow));
|
|
y += 1;
|
|
let name_str = format!("> {}\u{2588}", self.wizard.pipeline.model_name);
|
|
buf.set_string(
|
|
x,
|
|
y,
|
|
truncate(&name_str, w),
|
|
Style::default().fg(Color::Green),
|
|
);
|
|
y += 2;
|
|
buf.set_string(
|
|
x,
|
|
y,
|
|
"Enter to import, Esc to cancel",
|
|
Style::default().fg(Color::DarkGray),
|
|
);
|
|
|
|
if let Some(msg) = &self.wizard.message {
|
|
let msg_y = inner.y + inner.height - 1;
|
|
buf.set_string(x, msg_y, truncate(msg, w), Style::default().fg(Color::Red));
|
|
}
|
|
}
|
|
WizardStep::Done => {
|
|
buf.set_string(x, y, "Import complete!", Style::default().fg(Color::Green));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn truncate(s: &str, max: usize) -> String {
|
|
if s.len() <= max {
|
|
s.to_string()
|
|
} else if max > 1 {
|
|
format!("{}\u{2026}", &s[..max - 1])
|
|
} else {
|
|
s[..max].to_string()
|
|
}
|
|
}
|