diff --git a/crates/typedialog-core/src/nickel/roundtrip.rs b/crates/typedialog-core/src/nickel/roundtrip.rs index 2b44999..20dd6e3 100644 --- a/crates/typedialog-core/src/nickel/roundtrip.rs +++ b/crates/typedialog-core/src/nickel/roundtrip.rs @@ -396,6 +396,8 @@ impl RoundtripConfig { form_path: &Path, initial_values: HashMap, ) -> Result> { + use std::collections::BTreeMap; + // Read form definition let form_content = fs::read_to_string(form_path).map_err(|e| { crate::error::ErrorWrapper::new(format!("Failed to read form file: {}", e)) @@ -407,8 +409,15 @@ impl RoundtripConfig { // Migrate to unified elements format form.migrate_to_elements(); - // Merge initial values into form defaults (unified elements format) - for element in &mut form.elements { + // Extract base directory for resolving relative paths (includes, fragments) + let base_dir = form_path.parent().unwrap_or_else(|| Path::new(".")); + + // CRITICAL FIX: Expand fragments BEFORE applying defaults + // This ensures defaults are applied to real fields, not to groups with includes + let mut expanded_form = form_parser::expand_includes(form, base_dir)?; + + // Now apply initial values as defaults to the EXPANDED fields + for element in &mut expanded_form.elements { if let form_parser::FormElement::Field(field) = element { if let Some(value) = initial_values.get(&field.name) { // Set as default if field doesn't already have one @@ -419,12 +428,262 @@ impl RoundtripConfig { } } - // Extract base directory for resolving relative paths (includes, fragments) - let base_dir = form_path.parent().unwrap_or_else(|| Path::new(".")); + // Execute expanded form directly (without re-expanding includes) + // This is a simplified version of execute_with_base_dir that doesn't call expand_includes again + let mut results = HashMap::new(); - // Execute form using CLI backend (interactive prompts) - // This will expand includes from groups during execution - form_parser::execute_with_base_dir(form, base_dir) + // Print form header + if let Some(desc) = &expanded_form.description { + println!("\n{}\n{}\n", expanded_form.name, desc); + } else { + println!("\n{}\n", expanded_form.name); + } + + // Build ordered element map + let mut element_map: BTreeMap = BTreeMap::new(); + let mut order_counter = 0; + + for element in expanded_form.elements { + let order = match &element { + form_parser::FormElement::Item(item) => { + if item.order == 0 { + order_counter += 1; + order_counter - 1 + } else { + item.order + } + } + form_parser::FormElement::Field(field) => { + if field.order == 0 { + order_counter += 1; + order_counter - 1 + } else { + field.order + } + } + }; + element_map.insert(order, element); + } + + // Process elements in order (using CLI prompts) + for (_, element) in element_map.iter() { + match element { + form_parser::FormElement::Item(item) => { + form_parser::render_display_item(item, &results); + } + form_parser::FormElement::Field(field) => { + // Check if field should be shown based on conditional + if let Some(condition) = &field.when { + if !form_parser::evaluate_condition(condition, &results) { + // Field condition not met, skip it + continue; + } + } + + // Execute field using CLI prompts (from execute_with_base_dir logic) + let value = Self::execute_field_cli(field, &results)?; + results.insert(field.name.clone(), value.clone()); + } + } + } + + Ok(results) + } + + /// Execute a single field using CLI prompts (extracted from executor.rs) + fn execute_field_cli( + field: &form_parser::FieldDefinition, + previous_results: &HashMap, + ) -> Result { + use crate::prompts; + + let is_required = field.required.unwrap_or(false); + let required_marker = if is_required { " *" } else { " (optional)" }; + + match field.field_type { + form_parser::FieldType::Text => { + let prompt_with_marker = format!("{}{}", field.prompt, required_marker); + let result = prompts::text( + &prompt_with_marker, + field.default.as_deref(), + field.placeholder.as_deref(), + )?; + + if is_required && result.is_empty() { + eprintln!("⚠ This field is required. Please enter a value."); + return Self::execute_field_cli(field, previous_results); // Retry + } + Ok(serde_json::json!(result)) + } + + form_parser::FieldType::Confirm => { + let prompt_with_marker = format!("{}{}", field.prompt, required_marker); + let default_bool = + field + .default + .as_deref() + .and_then(|s| match s.to_lowercase().as_str() { + "true" | "yes" => Some(true), + "false" | "no" => Some(false), + _ => None, + }); + let result = prompts::confirm(&prompt_with_marker, default_bool, None)?; + Ok(serde_json::json!(result)) + } + + form_parser::FieldType::Password => { + let prompt_with_marker = format!("{}{}", field.prompt, required_marker); + let with_toggle = field.placeholder.as_deref() == Some("toggle"); + let result = prompts::password(&prompt_with_marker, with_toggle)?; + + if is_required && result.is_empty() { + eprintln!("⚠ This field is required. Please enter a value."); + return Self::execute_field_cli(field, previous_results); // Retry + } + Ok(serde_json::json!(result)) + } + + form_parser::FieldType::Select => { + if field.options.is_empty() { + return Err(crate::error::ErrorWrapper::new( + "Select field requires 'options'".to_string(), + )); + } + let prompt_with_marker = format!("{}{}", field.prompt, required_marker); + + // Filter options based on options_from if specified + let filtered_options = Self::filter_options_from(field, previous_results); + + if filtered_options.is_empty() { + return Err(crate::error::ErrorWrapper::new(format!( + "No options available for field '{}'. Check options_from reference.", + field.name + ))); + } + + let options = filtered_options + .iter() + .map(|opt| opt.as_string()) + .collect::>(); + let result = prompts::select( + &prompt_with_marker, + options, + field.page_size, + field.vim_mode.unwrap_or(false), + )?; + Ok(serde_json::json!(result)) + } + + form_parser::FieldType::MultiSelect => { + if field.options.is_empty() { + return Err(crate::error::ErrorWrapper::new( + "MultiSelect field requires 'options'".to_string(), + )); + } + let prompt_with_marker = format!("{}{}", field.prompt, required_marker); + let options = field + .options + .iter() + .map(|opt| opt.as_string()) + .collect::>(); + let results = prompts::multi_select( + &prompt_with_marker, + options, + field.page_size, + field.vim_mode.unwrap_or(false), + )?; + + if is_required && results.is_empty() { + eprintln!("⚠ This field is required. Please select at least one option."); + return Self::execute_field_cli(field, previous_results); // Retry + } + Ok(serde_json::json!(results)) + } + + form_parser::FieldType::Editor => { + let prompt_with_marker = format!("{}{}", field.prompt, required_marker); + let result = prompts::editor( + &prompt_with_marker, + field.file_extension.as_deref(), + field.prefix_text.as_deref(), + )?; + + if is_required && result.is_empty() { + eprintln!("⚠ This field is required. Please enter a value."); + return Self::execute_field_cli(field, previous_results); // Retry + } + Ok(serde_json::json!(result)) + } + + form_parser::FieldType::Date => { + let prompt_with_marker = format!("{}{}", field.prompt, required_marker); + let week_start = field.week_start.as_deref().unwrap_or("Mon"); + let result = prompts::date( + &prompt_with_marker, + field.default.as_deref(), + field.min_date.as_deref(), + field.max_date.as_deref(), + week_start, + )?; + Ok(serde_json::json!(result)) + } + + form_parser::FieldType::Custom => { + let prompt_with_marker = format!("{}{}", field.prompt, required_marker); + let type_name = field.custom_type.as_ref().ok_or_else(|| { + crate::error::ErrorWrapper::new( + "Custom field requires 'custom_type'".to_string(), + ) + })?; + let result = + prompts::custom(&prompt_with_marker, type_name, field.default.as_deref())?; + + if is_required && result.is_empty() { + eprintln!("⚠ This field is required. Please enter a value."); + return Self::execute_field_cli(field, previous_results); // Retry + } + Ok(serde_json::json!(result)) + } + + form_parser::FieldType::RepeatingGroup => Err(crate::error::ErrorWrapper::new( + "RepeatingGroup not yet implemented".to_string(), + )), + } + } + + /// Filter field options based on options_from reference (extracted from executor.rs) + fn filter_options_from( + field: &form_parser::FieldDefinition, + previous_results: &HashMap, + ) -> Vec { + // If no options_from specified, return all options + let Some(ref source_field) = field.options_from else { + return field.options.clone(); + }; + + // Get the source field's value + let Some(source_value) = previous_results.get(source_field) else { + // Source field not found, return all options + return field.options.clone(); + }; + + // Extract selected values from source field (could be array or comma-separated string) + let selected_values: Vec = match source_value { + Value::Array(arr) => arr + .iter() + .filter_map(|v| v.as_str().map(|s| s.to_string())) + .collect(), + Value::String(s) => s.split(',').map(|item| item.trim().to_string()).collect(), + _ => return field.options.clone(), // Unsupported type, return all + }; + + // Filter options to only include those in selected_values + field + .options + .iter() + .filter(|opt| selected_values.contains(&opt.value)) + .cloned() + .collect() } /// Load defaults from input Nickel file using form field nickel_path