624 lines
21 KiB
Rust
624 lines
21 KiB
Rust
// examples/computed_fields.rs - COMPLETE WORKING VERSION
|
||
//! Demonstrates computed fields with the canvas library - Invoice Calculator Example
|
||
//!
|
||
//! This example REQUIRES the `computed` feature to compile.
|
||
//!
|
||
//! Run with:
|
||
//! cargo run --example computed_fields --features "gui,computed"
|
||
|
||
#[cfg(not(feature = "computed"))]
|
||
compile_error!(
|
||
"This example requires the 'computed' feature. \
|
||
Run with: cargo run --example computed_fields --features \"gui,computed\""
|
||
);
|
||
|
||
use std::io;
|
||
use crossterm::{
|
||
event::{
|
||
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyModifiers,
|
||
},
|
||
execute,
|
||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||
};
|
||
use ratatui::{
|
||
backend::{Backend, CrosstermBackend},
|
||
layout::{Constraint, Direction, Layout, Rect},
|
||
style::{Color, Style, Modifier},
|
||
text::{Line, Span},
|
||
widgets::{Block, Borders, Paragraph, Wrap},
|
||
Frame, Terminal,
|
||
};
|
||
|
||
use canvas::{
|
||
canvas::{gui::render_canvas_default, modes::AppMode},
|
||
DataProvider, FormEditor,
|
||
computed::{ComputedProvider, ComputedContext},
|
||
};
|
||
|
||
/// Invoice data with computed fields
|
||
struct InvoiceData {
|
||
fields: Vec<(String, String)>,
|
||
computed_indices: std::collections::HashSet<usize>,
|
||
}
|
||
|
||
impl InvoiceData {
|
||
fn new() -> Self {
|
||
let mut computed_indices = std::collections::HashSet::new();
|
||
|
||
// Mark computed fields (read-only, calculated)
|
||
computed_indices.insert(4); // Subtotal
|
||
computed_indices.insert(5); // Tax Amount
|
||
computed_indices.insert(6); // Total
|
||
|
||
Self {
|
||
fields: vec![
|
||
("📦 Product Name".to_string(), "".to_string()),
|
||
("🔢 Quantity".to_string(), "".to_string()),
|
||
("💰 Unit Price ($)".to_string(), "".to_string()),
|
||
("📊 Tax Rate (%)".to_string(), "".to_string()),
|
||
("➕ Subtotal ($)".to_string(), "".to_string()), // COMPUTED
|
||
("🧾 Tax Amount ($)".to_string(), "".to_string()), // COMPUTED
|
||
("💳 Total ($)".to_string(), "".to_string()), // COMPUTED
|
||
("📝 Notes".to_string(), "".to_string()),
|
||
],
|
||
computed_indices,
|
||
}
|
||
}
|
||
}
|
||
|
||
impl DataProvider for InvoiceData {
|
||
fn field_count(&self) -> usize {
|
||
self.fields.len()
|
||
}
|
||
|
||
fn field_name(&self, index: usize) -> &str {
|
||
&self.fields[index].0
|
||
}
|
||
|
||
fn field_value(&self, index: usize) -> &str {
|
||
&self.fields[index].1
|
||
}
|
||
|
||
fn set_field_value(&mut self, index: usize, value: String) {
|
||
// 🔥 FIXED: Allow computed fields to be updated for display purposes
|
||
// The editing protection happens at the editor level, not here
|
||
self.fields[index].1 = value;
|
||
}
|
||
|
||
fn supports_suggestions(&self, _field_index: usize) -> bool {
|
||
false
|
||
}
|
||
|
||
fn display_value(&self, _index: usize) -> Option<&str> {
|
||
None
|
||
}
|
||
|
||
/// Mark computed fields
|
||
fn is_computed_field(&self, field_index: usize) -> bool {
|
||
self.computed_indices.contains(&field_index)
|
||
}
|
||
|
||
/// Get computed field values
|
||
fn computed_field_value(&self, field_index: usize) -> Option<String> {
|
||
if self.computed_indices.contains(&field_index) {
|
||
Some(self.fields[field_index].1.clone())
|
||
} else {
|
||
None
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Invoice calculator - computes totals based on input fields
|
||
struct InvoiceCalculator;
|
||
|
||
impl ComputedProvider for InvoiceCalculator {
|
||
fn compute_field(&mut self, context: ComputedContext) -> String {
|
||
// Helper to parse field values safely
|
||
let parse_field = |index: usize| -> f64 {
|
||
let value = context.field_values[index].trim();
|
||
if value.is_empty() {
|
||
0.0
|
||
} else {
|
||
value.parse().unwrap_or(0.0)
|
||
}
|
||
};
|
||
|
||
match context.target_field {
|
||
4 => {
|
||
// Subtotal = Quantity × Unit Price
|
||
let qty = parse_field(1);
|
||
let price = parse_field(2);
|
||
let subtotal = qty * price;
|
||
|
||
if qty == 0.0 || price == 0.0 {
|
||
"".to_string() // Show empty if no meaningful calculation
|
||
} else {
|
||
format!("{:.2}", subtotal)
|
||
}
|
||
}
|
||
5 => {
|
||
// Tax Amount = Subtotal × (Tax Rate / 100)
|
||
let qty = parse_field(1);
|
||
let price = parse_field(2);
|
||
let tax_rate = parse_field(3);
|
||
let subtotal = qty * price;
|
||
let tax_amount = subtotal * (tax_rate / 100.0);
|
||
|
||
if subtotal == 0.0 || tax_rate == 0.0 {
|
||
"".to_string()
|
||
} else {
|
||
format!("{:.2}", tax_amount)
|
||
}
|
||
}
|
||
6 => {
|
||
// Total = Subtotal + Tax Amount
|
||
let qty = parse_field(1);
|
||
let price = parse_field(2);
|
||
let tax_rate = parse_field(3);
|
||
let subtotal = qty * price;
|
||
|
||
if subtotal == 0.0 {
|
||
"".to_string()
|
||
} else {
|
||
let tax_amount = subtotal * (tax_rate / 100.0);
|
||
let total = subtotal + tax_amount;
|
||
format!("{:.2}", total)
|
||
}
|
||
}
|
||
_ => "".to_string(),
|
||
}
|
||
}
|
||
|
||
fn handles_field(&self, field_index: usize) -> bool {
|
||
matches!(field_index, 4 | 5 | 6) // Subtotal, Tax Amount, Total
|
||
}
|
||
|
||
fn field_dependencies(&self, field_index: usize) -> Vec<usize> {
|
||
match field_index {
|
||
4 => vec![1, 2], // Subtotal depends on Quantity, Unit Price
|
||
5 => vec![1, 2, 3], // Tax Amount depends on Quantity, Unit Price, Tax Rate
|
||
6 => vec![1, 2, 3], // Total depends on Quantity, Unit Price, Tax Rate
|
||
_ => vec![],
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Enhanced editor with computed fields
|
||
struct ComputedFieldsEditor<D: DataProvider> {
|
||
editor: FormEditor<D>,
|
||
calculator: InvoiceCalculator,
|
||
debug_message: String,
|
||
last_computed_values: Vec<String>,
|
||
}
|
||
|
||
impl<D: DataProvider> ComputedFieldsEditor<D> {
|
||
fn new(data_provider: D) -> Self {
|
||
let mut editor = FormEditor::new(data_provider);
|
||
editor.set_computed_provider(InvoiceCalculator);
|
||
|
||
let calculator = InvoiceCalculator;
|
||
let last_computed_values = vec!["".to_string(); 8];
|
||
|
||
Self {
|
||
editor,
|
||
calculator,
|
||
debug_message: "💰 Invoice Calculator - Start typing in fields to see calculations!".to_string(),
|
||
last_computed_values,
|
||
}
|
||
}
|
||
|
||
fn is_computed_field(&self, field_index: usize) -> bool {
|
||
self.editor.ui_state().is_computed_field(field_index)
|
||
}
|
||
|
||
fn update_computed_fields(&mut self) {
|
||
// Trigger recomputation of all computed fields
|
||
self.editor.recompute_all_fields(&mut self.calculator);
|
||
|
||
// 🔥 CRITICAL FIX: Sync computed values to DataProvider so GUI shows them!
|
||
for i in [4, 5, 6] { // Computed field indices
|
||
let computed_value = self.editor.effective_field_value(i);
|
||
self.editor.data_provider_mut().set_field_value(i, computed_value.clone());
|
||
}
|
||
|
||
// Check if values changed to show feedback
|
||
let mut changed = false;
|
||
let mut has_calculations = false;
|
||
|
||
for i in [4, 5, 6] {
|
||
let new_value = self.editor.effective_field_value(i);
|
||
if new_value != self.last_computed_values[i] {
|
||
changed = true;
|
||
self.last_computed_values[i] = new_value.clone();
|
||
}
|
||
if !new_value.is_empty() {
|
||
has_calculations = true;
|
||
}
|
||
}
|
||
|
||
if changed {
|
||
if has_calculations {
|
||
let subtotal = &self.last_computed_values[4];
|
||
let tax = &self.last_computed_values[5];
|
||
let total = &self.last_computed_values[6];
|
||
|
||
let mut parts = Vec::new();
|
||
if !subtotal.is_empty() {
|
||
parts.push(format!("Subtotal=${}", subtotal));
|
||
}
|
||
if !tax.is_empty() {
|
||
parts.push(format!("Tax=${}", tax));
|
||
}
|
||
if !total.is_empty() {
|
||
parts.push(format!("Total=${}", total));
|
||
}
|
||
|
||
if !parts.is_empty() {
|
||
self.debug_message = format!("🧮 Calculated: {}", parts.join(", "));
|
||
} else {
|
||
self.debug_message = "💰 Enter Quantity and Unit Price to see calculations".to_string();
|
||
}
|
||
} else {
|
||
self.debug_message = "💰 Enter Quantity and Unit Price to see calculations".to_string();
|
||
}
|
||
}
|
||
}
|
||
|
||
fn insert_char(&mut self, ch: char) -> anyhow::Result<()> {
|
||
let current_field = self.editor.current_field();
|
||
let result = self.editor.insert_char(ch);
|
||
|
||
if result.is_ok() && matches!(current_field, 1 | 2 | 3) {
|
||
self.editor.on_field_changed(&mut self.calculator, current_field);
|
||
self.update_computed_fields();
|
||
}
|
||
|
||
result
|
||
}
|
||
|
||
fn delete_backward(&mut self) -> anyhow::Result<()> {
|
||
let current_field = self.editor.current_field();
|
||
let result = self.editor.delete_backward();
|
||
|
||
if result.is_ok() && matches!(current_field, 1 | 2 | 3) {
|
||
self.editor.on_field_changed(&mut self.calculator, current_field);
|
||
self.update_computed_fields();
|
||
}
|
||
|
||
result
|
||
}
|
||
|
||
fn delete_forward(&mut self) -> anyhow::Result<()> {
|
||
let current_field = self.editor.current_field();
|
||
let result = self.editor.delete_forward();
|
||
|
||
if result.is_ok() && matches!(current_field, 1 | 2 | 3) {
|
||
self.editor.on_field_changed(&mut self.calculator, current_field);
|
||
self.update_computed_fields();
|
||
}
|
||
|
||
result
|
||
}
|
||
|
||
fn next_field(&mut self) {
|
||
let old_field = self.editor.current_field();
|
||
let _ = self.editor.next_field();
|
||
let new_field = self.editor.current_field();
|
||
|
||
if old_field != new_field {
|
||
let field_name = self.editor.data_provider().field_name(new_field);
|
||
let field_type = if self.is_computed_field(new_field) {
|
||
"computed (read-only)"
|
||
} else {
|
||
"editable"
|
||
};
|
||
self.debug_message = format!("→ {} - {} field", field_name, field_type);
|
||
}
|
||
}
|
||
|
||
fn prev_field(&mut self) {
|
||
let old_field = self.editor.current_field();
|
||
let _ = self.editor.prev_field();
|
||
let new_field = self.editor.current_field();
|
||
|
||
if old_field != new_field {
|
||
let field_name = self.editor.data_provider().field_name(new_field);
|
||
let field_type = if self.is_computed_field(new_field) {
|
||
"computed (read-only)"
|
||
} else {
|
||
"editable"
|
||
};
|
||
self.debug_message = format!("← {} - {} field", field_name, field_type);
|
||
}
|
||
}
|
||
|
||
fn enter_edit_mode(&mut self) {
|
||
let current = self.editor.current_field();
|
||
|
||
// Double protection: check both ways
|
||
if self.editor.data_provider().is_computed_field(current) || self.is_computed_field(current) {
|
||
let field_name = self.editor.data_provider().field_name(current);
|
||
self.debug_message = format!(
|
||
"🚫 {} is computed (read-only) - Press Tab to move to editable fields",
|
||
field_name
|
||
);
|
||
return;
|
||
}
|
||
|
||
self.editor.enter_edit_mode();
|
||
let field_name = self.editor.data_provider().field_name(current);
|
||
self.debug_message = format!("✏️ Editing {} - Type to see calculations update", field_name);
|
||
}
|
||
|
||
fn enter_append_mode(&mut self) {
|
||
let current = self.editor.current_field();
|
||
|
||
if self.editor.data_provider().is_computed_field(current) || self.is_computed_field(current) {
|
||
let field_name = self.editor.data_provider().field_name(current);
|
||
self.debug_message = format!(
|
||
"🚫 {} is computed (read-only) - Press Tab to move to editable fields",
|
||
field_name
|
||
);
|
||
return;
|
||
}
|
||
|
||
self.editor.enter_append_mode();
|
||
let field_name = self.editor.data_provider().field_name(current);
|
||
self.debug_message = format!("✏️ Appending to {} - Type to see calculations", field_name);
|
||
}
|
||
|
||
fn exit_edit_mode(&mut self) {
|
||
let current_field = self.editor.current_field();
|
||
self.editor.exit_edit_mode();
|
||
|
||
if matches!(current_field, 1 | 2 | 3) {
|
||
self.editor.on_field_changed(&mut self.calculator, current_field);
|
||
self.update_computed_fields();
|
||
}
|
||
|
||
self.debug_message = "🔒 Normal mode - Press 'i' to edit fields".to_string();
|
||
}
|
||
|
||
// Delegate methods
|
||
fn current_field(&self) -> usize { self.editor.current_field() }
|
||
fn cursor_position(&self) -> usize { self.editor.cursor_position() }
|
||
fn mode(&self) -> AppMode { self.editor.mode() }
|
||
fn current_text(&self) -> &str {
|
||
let field_index = self.editor.current_field();
|
||
self.editor.data_provider().field_value(field_index)
|
||
}
|
||
fn data_provider(&self) -> &D { self.editor.data_provider() }
|
||
fn ui_state(&self) -> &canvas::EditorState { self.editor.ui_state() }
|
||
fn move_left(&mut self) { self.editor.move_left(); }
|
||
fn move_right(&mut self) { self.editor.move_right(); }
|
||
fn move_up(&mut self) { let _ = self.editor.move_up(); }
|
||
fn move_down(&mut self) { let _ = self.editor.move_down(); }
|
||
}
|
||
|
||
fn handle_key_press(
|
||
key: KeyCode,
|
||
modifiers: KeyModifiers,
|
||
editor: &mut ComputedFieldsEditor<InvoiceData>,
|
||
) -> anyhow::Result<bool> {
|
||
let mode = editor.mode();
|
||
|
||
if (key == KeyCode::Char('q') && modifiers.contains(KeyModifiers::CONTROL))
|
||
|| (key == KeyCode::Char('c') && modifiers.contains(KeyModifiers::CONTROL))
|
||
|| key == KeyCode::F(10)
|
||
{
|
||
return Ok(false);
|
||
}
|
||
|
||
match (mode, key, modifiers) {
|
||
(AppMode::ReadOnly, KeyCode::Char('i'), _) => {
|
||
editor.enter_edit_mode();
|
||
}
|
||
(AppMode::ReadOnly, KeyCode::Char('a'), _) => {
|
||
editor.enter_append_mode();
|
||
}
|
||
(AppMode::ReadOnly, KeyCode::Char('A'), _) => {
|
||
editor.editor.move_line_end();
|
||
editor.enter_edit_mode();
|
||
}
|
||
(_, KeyCode::Esc, _) => {
|
||
if mode == AppMode::Edit {
|
||
editor.exit_edit_mode();
|
||
}
|
||
}
|
||
|
||
// Movement
|
||
(AppMode::ReadOnly, KeyCode::Char('h'), _) | (AppMode::ReadOnly, KeyCode::Left, _) => {
|
||
editor.move_left();
|
||
}
|
||
(AppMode::ReadOnly, KeyCode::Char('l'), _) | (AppMode::ReadOnly, KeyCode::Right, _) => {
|
||
editor.move_right();
|
||
}
|
||
(AppMode::ReadOnly, KeyCode::Char('j'), _) | (AppMode::ReadOnly, KeyCode::Down, _) => {
|
||
editor.move_down();
|
||
}
|
||
(AppMode::ReadOnly, KeyCode::Char('k'), _) | (AppMode::ReadOnly, KeyCode::Up, _) => {
|
||
editor.move_up();
|
||
}
|
||
|
||
// Edit mode movement
|
||
(AppMode::Edit, KeyCode::Left, _) => { editor.move_left(); }
|
||
(AppMode::Edit, KeyCode::Right, _) => { editor.move_right(); }
|
||
(AppMode::Edit, KeyCode::Up, _) => { editor.move_up(); }
|
||
(AppMode::Edit, KeyCode::Down, _) => { editor.move_down(); }
|
||
|
||
// Navigation
|
||
(_, KeyCode::Tab, _) => {
|
||
editor.next_field();
|
||
}
|
||
(_, KeyCode::BackTab, _) => {
|
||
editor.prev_field();
|
||
}
|
||
|
||
// Editing
|
||
(AppMode::Edit, KeyCode::Char(c), m) if !m.contains(KeyModifiers::CONTROL) => {
|
||
editor.insert_char(c)?;
|
||
}
|
||
(AppMode::Edit, KeyCode::Backspace, _) => {
|
||
editor.delete_backward()?;
|
||
}
|
||
(AppMode::Edit, KeyCode::Delete, _) => {
|
||
editor.delete_forward()?;
|
||
}
|
||
|
||
// Debug info
|
||
(AppMode::ReadOnly, KeyCode::Char('?'), _) => {
|
||
let current = editor.current_field();
|
||
let field_name = editor.data_provider().field_name(current);
|
||
let field_type = if editor.is_computed_field(current) {
|
||
"COMPUTED (read-only)"
|
||
} else {
|
||
"EDITABLE"
|
||
};
|
||
editor.debug_message = format!(
|
||
"{} - {} - Position {} - Mode: {:?}",
|
||
field_name, field_type, editor.cursor_position(), mode
|
||
);
|
||
}
|
||
|
||
_ => {}
|
||
}
|
||
|
||
Ok(true)
|
||
}
|
||
|
||
fn run_app<B: Backend>(
|
||
terminal: &mut Terminal<B>,
|
||
mut editor: ComputedFieldsEditor<InvoiceData>,
|
||
) -> io::Result<()> {
|
||
editor.update_computed_fields(); // Initial computation
|
||
|
||
loop {
|
||
terminal.draw(|f| ui(f, &editor))?;
|
||
|
||
if let Event::Key(key) = event::read()? {
|
||
match handle_key_press(key.code, key.modifiers, &mut editor) {
|
||
Ok(should_continue) => {
|
||
if !should_continue {
|
||
break;
|
||
}
|
||
}
|
||
Err(e) => {
|
||
editor.debug_message = format!("Error: {}", e);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
fn ui(f: &mut Frame, editor: &ComputedFieldsEditor<InvoiceData>) {
|
||
let chunks = Layout::default()
|
||
.direction(Direction::Vertical)
|
||
.constraints([Constraint::Min(8), Constraint::Length(10)])
|
||
.split(f.area());
|
||
|
||
render_enhanced_canvas(f, chunks[0], editor);
|
||
render_computed_status(f, chunks[1], editor);
|
||
}
|
||
|
||
fn render_enhanced_canvas(f: &mut Frame, area: Rect, editor: &ComputedFieldsEditor<InvoiceData>) {
|
||
render_canvas_default(f, area, &editor.editor);
|
||
}
|
||
|
||
fn render_computed_status(f: &mut Frame, area: Rect, editor: &ComputedFieldsEditor<InvoiceData>) {
|
||
let chunks = Layout::default()
|
||
.direction(Direction::Vertical)
|
||
.constraints([Constraint::Length(3), Constraint::Length(7)])
|
||
.split(area);
|
||
|
||
let mode_text = match editor.mode() {
|
||
AppMode::Edit => "INSERT",
|
||
AppMode::ReadOnly => "NORMAL",
|
||
_ => "OTHER",
|
||
};
|
||
|
||
let current = editor.current_field();
|
||
let field_status = if editor.is_computed_field(current) {
|
||
"📊 COMPUTED FIELD (read-only)"
|
||
} else {
|
||
"✏️ EDITABLE FIELD"
|
||
};
|
||
|
||
let status_text = format!("-- {} -- {} | {}", mode_text, field_status, editor.debug_message);
|
||
|
||
let status = Paragraph::new(Line::from(Span::raw(status_text)))
|
||
.block(Block::default().borders(Borders::ALL).title("💰 Invoice Calculator"));
|
||
|
||
f.render_widget(status, chunks[0]);
|
||
|
||
let help_text = match editor.mode() {
|
||
AppMode::ReadOnly => {
|
||
"💰 COMPUTED FIELDS DEMO: Real-time invoice calculations!\n\
|
||
🔢 EDITABLE: Product, Quantity, Unit Price, Tax Rate, Notes\n\
|
||
📊 COMPUTED: Subtotal, Tax Amount, Total (calculated automatically)\n\
|
||
\n\
|
||
🚀 START: Press 'i' to edit Quantity, type '5', Tab to Unit Price, type '19.99'\n\
|
||
Watch Subtotal and Total appear! Add Tax Rate to see tax calculations.\n\
|
||
Navigation: Tab/Shift+Tab skips computed fields automatically"
|
||
}
|
||
AppMode::Edit => {
|
||
"✏️ EDIT MODE: Type numbers to see calculations appear!\n\
|
||
\n\
|
||
💡 EXAMPLE: Type '5' in Quantity, then Tab to Unit Price and type '19.99'\n\
|
||
• Subtotal appears: $99.95\n\
|
||
• Total appears: $99.95\n\
|
||
• Add Tax Rate (like '10') to see tax: $9.99, Total: $109.94\n\
|
||
\n\
|
||
Esc=normal, Tab=next field (auto-skips computed fields)"
|
||
}
|
||
_ => "💰 Invoice Calculator with Computed Fields"
|
||
};
|
||
|
||
let help_style = if editor.is_computed_field(editor.current_field()) {
|
||
Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC)
|
||
} else {
|
||
Style::default().fg(Color::Gray)
|
||
};
|
||
|
||
let help = Paragraph::new(help_text)
|
||
.block(Block::default().borders(Borders::ALL).title("🚀 Try It Now!"))
|
||
.style(help_style)
|
||
.wrap(Wrap { trim: true });
|
||
|
||
f.render_widget(help, chunks[1]);
|
||
}
|
||
|
||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||
println!("💰 Canvas Computed Fields Demo - Invoice Calculator");
|
||
println!("✅ computed feature: ENABLED");
|
||
println!("🚀 QUICK TEST:");
|
||
println!(" 1. Press 'i' to edit Quantity");
|
||
println!(" 2. Type '5' and press Tab");
|
||
println!(" 3. Type '19.99' in Unit Price");
|
||
println!(" 4. Watch Subtotal ($99.95) and Total ($99.95) appear!");
|
||
println!();
|
||
|
||
enable_raw_mode()?;
|
||
let mut stdout = io::stdout();
|
||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||
let backend = CrosstermBackend::new(stdout);
|
||
let mut terminal = Terminal::new(backend)?;
|
||
|
||
let data = InvoiceData::new();
|
||
let editor = ComputedFieldsEditor::new(data);
|
||
|
||
let res = run_app(&mut terminal, editor);
|
||
|
||
disable_raw_mode()?;
|
||
execute!(terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture)?;
|
||
terminal.show_cursor()?;
|
||
|
||
if let Err(err) = res {
|
||
println!("{:?}", err);
|
||
}
|
||
|
||
println!("💰 Demo completed! Computed fields should have updated in real-time!");
|
||
Ok(())
|
||
}
|