diff --git a/crates/typedialog-core/src/backends/web/mod.rs b/crates/typedialog-core/src/backends/web/mod.rs index ff04e01..515817f 100644 --- a/crates/typedialog-core/src/backends/web/mod.rs +++ b/crates/typedialog-core/src/backends/web/mod.rs @@ -535,11 +535,102 @@ async fn index_handler(State(state): State>) -> impl IntoRespo script.textContent = content; document.body.appendChild(script); }}); + + // After form is loaded and scripts executed, filter dependent options + // This ensures primary_language only shows languages selected in detected_languages + setTimeout(function() {{ + if (typeof filterDependentOptions === 'function') {{ + filterDependentOptions(); + }} + }}, 100); }}); }}); + // Dynamic options filtering based on data-options-from + function filterDependentOptions() {{ + // First, update all hidden fields for multiselect from their checkboxes + document.querySelectorAll('[id^="values_"]').forEach(hiddenField => {{ + const fieldName = hiddenField.id.replace('values_', ''); + const checkboxes = document.querySelectorAll('[data-checkbox-group="' + fieldName + '"]:checked'); + const values = Array.from(checkboxes).map(cb => cb.value); + hiddenField.value = values.join(','); + }}); + + // Find all select fields with data-options-from attribute + document.querySelectorAll('select[data-options-from]').forEach(targetSelect => {{ + const sourceFieldName = targetSelect.getAttribute('data-options-from'); + + // Store original options if not already stored + if (!targetSelect.dataset.allOptions) {{ + const allOptions = Array.from(targetSelect.options).map(opt => ({{ + value: opt.value, + text: opt.textContent + }})); + targetSelect.dataset.allOptions = JSON.stringify(allOptions); + }} + + // Find the source field (could be multiselect checkboxes or select) + const sourceHiddenField = document.getElementById('values_' + sourceFieldName); + const sourceSelect = document.querySelector('select[data-fieldname="' + sourceFieldName + '"]'); + + let selectedValues = []; + + if (sourceHiddenField) {{ + // Multiselect with checkboxes + const valuesStr = sourceHiddenField.value; + selectedValues = valuesStr ? valuesStr.split(',').map(v => v.trim()) : []; + }} else if (sourceSelect) {{ + // Regular select + selectedValues = [sourceSelect.value]; + }} + + // Get all original options + const allOptions = JSON.parse(targetSelect.dataset.allOptions); + + // Filter options to only include those in selectedValues + const filteredOptions = allOptions.filter(opt => selectedValues.includes(opt.value)); + + // Save current value - check both selected option and current value + let currentValue = targetSelect.value; + if (!currentValue) {{ + // If no value selected yet, check for option with 'selected' attribute + const selectedOption = targetSelect.querySelector('option[selected]'); + if (selectedOption) {{ + currentValue = selectedOption.value; + }} + }} + + // Clear and repopulate options + targetSelect.innerHTML = ''; + + // Determine which value should be selected + let valueToSelect = ''; + if (currentValue && filteredOptions.some(opt => opt.value === currentValue)) {{ + valueToSelect = currentValue; + }} else if (filteredOptions.length > 0) {{ + valueToSelect = filteredOptions[0].value; + }} + + // Rebuild options and mark the selected one + filteredOptions.forEach(opt => {{ + const option = document.createElement('option'); + option.value = opt.value; + option.textContent = opt.text; + if (opt.value === valueToSelect) {{ + option.selected = true; + }} + targetSelect.appendChild(option); + }}); + }}); + }} + // Reactive form field updates document.addEventListener('change', function(e) {{ + // First, handle dynamic options filtering + if (e.target.matches('input[type=checkbox][data-checkbox-group], select')) {{ + filterDependentOptions(); + }} + if (e.target.matches('select, input[type=checkbox], input[type=radio]')) {{ const form = document.querySelector('form#complete-form'); if (!form) return; @@ -559,6 +650,8 @@ async fn index_handler(State(state): State>) -> impl IntoRespo const values = Array.from(checkboxes).map(cb => cb.value); hiddenField.value = values.join(','); }}); + // Re-run option filtering after dynamic update + filterDependentOptions(); }}) .catch(err => console.error('Failed to update form:', err)); }} @@ -1348,17 +1441,25 @@ fn render_field_for_complete_form( .join("\n") }; + let options_from_attr = if let Some(ref source_field) = field.options_from { + format!(" data-options-from=\"{}\"", html_escape(source_field)) + } else { + String::new() + }; + ( format!( r#"
- {}
"#, html_escape(&field.prompt), html_escape(&field.name), + html_escape(&field.name), + options_from_attr, if field.required.unwrap_or(false) { "required" } else { @@ -1889,7 +1990,7 @@ fn render_repeating_group_field( .elements .iter() .filter_map(|element| match element { - crate::form_parser::FormElement::Field(f) => Some(f), + crate::form_parser::FormElement::Field(f) => Some(f.as_ref()), _ => None, }) .collect(); @@ -2680,10 +2781,16 @@ fn render_add_item_section( } else { "" }; + let options_from_attr = if let Some(ref source_field) = field.options_from { + format!(" data-options-from=\"{}\"", html_escape(source_field)) + } else { + String::new() + }; html.push_str(&format!( - r#""#, field.name, - required_attr + required_attr, + options_from_attr )); for opt in &field.options { html.push_str(&format!( diff --git a/crates/typedialog-core/src/encryption_bridge.rs b/crates/typedialog-core/src/encryption_bridge.rs index 341061c..1de0657 100644 --- a/crates/typedialog-core/src/encryption_bridge.rs +++ b/crates/typedialog-core/src/encryption_bridge.rs @@ -188,6 +188,7 @@ mod tests { default: None, placeholder: None, options: Vec::new(), + options_from: None, required: None, file_extension: None, prefix_text: None, diff --git a/crates/typedialog-core/src/form_parser/conditions.rs b/crates/typedialog-core/src/form_parser/conditions.rs index c9ed9fd..fd0421a 100644 --- a/crates/typedialog-core/src/form_parser/conditions.rs +++ b/crates/typedialog-core/src/form_parser/conditions.rs @@ -12,11 +12,12 @@ use super::types::FormDefinition; /// - "enable_prometheus == true" → Some("enable_prometheus") /// - "provider == lxd" → Some("provider") /// - "grafana_port >= 3000" → Some("grafana_port") +/// - "rust in detected_languages" → Some("detected_languages") pub(super) fn extract_field_from_condition(condition: &str) -> Option { let condition = condition.trim(); - // String operators: contains, startswith, endswith - let string_operators = ["contains", "startswith", "endswith"]; + // String operators: contains, startswith, endswith, in + let string_operators = ["contains", "startswith", "endswith", "in"]; for op_str in &string_operators { if let Some(pos) = condition.find(op_str) { let before_ok = pos == 0 @@ -33,7 +34,12 @@ pub(super) fn extract_field_from_condition(condition: &str) -> Option { .is_alphanumeric(); if before_ok && after_ok { - let field_name = condition[..pos].trim(); + // For "in" operator, the field is on the right side + let field_name = if *op_str == "in" { + condition[pos + op_str.len()..].trim() + } else { + condition[..pos].trim() + }; return Some(field_name.to_string()); } } @@ -113,11 +119,12 @@ pub fn should_load_fragment( /// - "field_name != value" /// - "field_name contains value" /// - "field_name startswith value" +/// - "value in field_name" (array membership) pub fn evaluate_condition(condition: &str, results: &HashMap) -> bool { let condition = condition.trim(); // Check string operators first (word boundaries) - let string_operators = ["contains", "startswith", "endswith"]; + let string_operators = ["contains", "startswith", "endswith", "in"]; for op_str in &string_operators { if let Some(pos) = condition.find(op_str) { // Make sure it's word-bounded (not part of another word) @@ -138,19 +145,53 @@ pub fn evaluate_condition(condition: &str, results: &HashMap return field_str.contains(&expected_str), - "startswith" => return field_str.starts_with(&expected_str), - "endswith" => return field_str.ends_with(&expected_str), - _ => {} + "in" => { + // For "in" operator: "value in array_field" + // left = value to search for + // right = field name containing array + let field_value = results + .get(right) + .cloned() + .unwrap_or(serde_json::Value::Null); + let search_value = left; + + // Handle array membership check + match &field_value { + serde_json::Value::Array(arr) => { + // Check if any array element matches the search value + return arr.iter().any(|v| { + let v_str = value_to_string(v); + v_str == search_value + }); + } + serde_json::Value::String(s) => { + // Handle comma-separated string as array (for multiselect defaults) + return s + .split(',') + .map(|item| item.trim()) + .any(|item| item == search_value); + } + _ => return false, + } + } + _ => { + // For other operators: "field_name operator value" + let field_value = results + .get(left) + .cloned() + .unwrap_or(serde_json::Value::Null); + let field_str = value_to_string(&field_value); + let expected = parse_condition_value(right); + let expected_str = value_to_string(&expected); + + match *op_str { + "contains" => return field_str.contains(&expected_str), + "startswith" => return field_str.starts_with(&expected_str), + "endswith" => return field_str.ends_with(&expected_str), + _ => {} + } + } } } } @@ -307,3 +348,123 @@ pub fn value_to_string(v: &serde_json::Value) -> String { other => other.to_string(), } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_in_operator_with_array() { + let mut results = HashMap::new(); + results.insert( + "detected_languages".to_string(), + serde_json::json!(["rust", "python", "go"]), + ); + + // Should find rust + assert!(evaluate_condition("rust in detected_languages", &results)); + + // Should find python + assert!(evaluate_condition("python in detected_languages", &results)); + + // Should not find javascript + assert!(!evaluate_condition( + "javascript in detected_languages", + &results + )); + } + + #[test] + fn test_in_operator_with_comma_separated_string() { + let mut results = HashMap::new(); + results.insert( + "detected_languages".to_string(), + serde_json::json!("rust,python,go"), + ); + + // Should find rust (with proper trimming) + assert!(evaluate_condition("rust in detected_languages", &results)); + + // Should find python + assert!(evaluate_condition("python in detected_languages", &results)); + + // Should not find javascript + assert!(!evaluate_condition( + "javascript in detected_languages", + &results + )); + } + + #[test] + fn test_in_operator_with_spaces() { + let mut results = HashMap::new(); + results.insert( + "detected_languages".to_string(), + serde_json::json!(["rust", "nushell", "nickel"]), + ); + + // Should work with spaces around "in" + assert!(evaluate_condition("rust in detected_languages", &results)); + assert!(evaluate_condition( + " rust in detected_languages ", + &results + )); + } + + #[test] + fn test_in_operator_not_found() { + let mut results = HashMap::new(); + results.insert( + "detected_languages".to_string(), + serde_json::json!(["rust", "python"]), + ); + + // Should not find when value not in array + assert!(!evaluate_condition( + "javascript in detected_languages", + &results + )); + } + + #[test] + fn test_in_operator_with_empty_array() { + let mut results = HashMap::new(); + results.insert("detected_languages".to_string(), serde_json::json!([])); + + // Should not find anything in empty array + assert!(!evaluate_condition("rust in detected_languages", &results)); + } + + #[test] + fn test_in_operator_with_missing_field() { + let results = HashMap::new(); + + // Should not find when field doesn't exist + assert!(!evaluate_condition("rust in detected_languages", &results)); + } + + #[test] + fn test_extract_field_from_in_condition() { + // For "in" operator, should extract the array field name (right side) + assert_eq!( + extract_field_from_condition("rust in detected_languages"), + Some("detected_languages".to_string()) + ); + + assert_eq!( + extract_field_from_condition("python in languages"), + Some("languages".to_string()) + ); + } + + #[test] + fn test_existing_operators_still_work() { + let mut results = HashMap::new(); + results.insert("enable_feature".to_string(), serde_json::json!(true)); + results.insert("name".to_string(), serde_json::json!("test")); + + // Existing operators should still work + assert!(evaluate_condition("enable_feature == true", &results)); + assert!(evaluate_condition("name == test", &results)); + } +} diff --git a/crates/typedialog-core/src/form_parser/executor.rs b/crates/typedialog-core/src/form_parser/executor.rs index 1601ce2..213b1d4 100644 --- a/crates/typedialog-core/src/form_parser/executor.rs +++ b/crates/typedialog-core/src/form_parser/executor.rs @@ -174,6 +174,41 @@ pub fn load_and_execute_from_file( execute_with_base_dir(form, base_dir) } +/// Filter field options based on options_from reference +fn filter_options_from( + field: &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 { + serde_json::Value::Array(arr) => arr + .iter() + .filter_map(|v| v.as_str().map(|s| s.to_string())) + .collect(), + serde_json::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() +} + /// Execute a single field fn execute_field( field: &FieldDefinition, @@ -232,8 +267,18 @@ fn execute_field( )); } let prompt_with_marker = format!("{}{}", field.prompt, required_marker); - let options = field - .options + + // Filter options based on options_from if specified + let filtered_options = filter_options_from(field, _previous_results); + + if filtered_options.is_empty() { + return Err(crate::ErrorWrapper::form_parse_failed(format!( + "No options available for field '{}'. Check options_from reference.", + field.name + ))); + } + + let options = filtered_options .iter() .map(|opt| opt.as_string()) .collect::>(); @@ -336,7 +381,7 @@ fn build_element_list( for element in form.elements.iter() { match element { FormElement::Item(item) => { - let mut item_clone = item.clone(); + let mut item_clone = item.as_ref().clone(); // Handle group type with includes if item.item_type == "group" { @@ -387,15 +432,15 @@ fn build_element_list( // Non-group items get order from position counter (insertion order) item_clone.order = order_counter; order_counter += 1; - element_list.push((item_clone.order, FormElement::Item(item_clone))); + element_list.push((item_clone.order, FormElement::Item(Box::new(item_clone)))); } } FormElement::Field(field) => { - let mut field_clone = field.clone(); + let mut field_clone = field.as_ref().clone(); // Assign order based on position counter (insertion order) field_clone.order = order_counter; order_counter += 1; - element_list.push((field_clone.order, FormElement::Field(field_clone))); + element_list.push((field_clone.order, FormElement::Field(Box::new(field_clone)))); } } } @@ -448,7 +493,7 @@ pub fn recompute_visible_elements( .is_none_or(|cond| evaluate_condition(cond, results)); if should_show { - visible_items.push(item); + visible_items.push(*item); } } FormElement::Field(field) => { @@ -459,7 +504,7 @@ pub fn recompute_visible_elements( .is_none_or(|cond| evaluate_condition(cond, results)); if should_show { - visible_fields.push(field); + visible_fields.push(*field); } } } @@ -530,7 +575,7 @@ pub async fn execute_with_backend_complete( let items: Vec<&DisplayItem> = element_list .iter() .filter_map(|(_, e)| match e { - FormElement::Item(item) => Some(item), + FormElement::Item(item) => Some(item.as_ref()), _ => None, }) .collect(); @@ -543,7 +588,7 @@ pub async fn execute_with_backend_complete( if selector_field_names.contains(&field.name) { return None; } - Some(field) + Some(field.as_ref()) } _ => None, }) @@ -795,7 +840,7 @@ pub async fn execute_with_backend_i18n_with_defaults( return None; } } - Some(item) + Some(item.as_ref()) } _ => None, }) @@ -812,7 +857,7 @@ pub async fn execute_with_backend_i18n_with_defaults( return None; } } - Some(field) + Some(field.as_ref()) } _ => None, }) diff --git a/crates/typedialog-core/src/form_parser/types.rs b/crates/typedialog-core/src/form_parser/types.rs index 48df492..ff6eb7e 100644 --- a/crates/typedialog-core/src/form_parser/types.rs +++ b/crates/typedialog-core/src/form_parser/types.rs @@ -30,15 +30,15 @@ where /// Public enum for unified form structure #[derive(Debug, Clone)] pub enum FormElement { - Item(DisplayItem), - Field(FieldDefinition), + Item(Box), + Field(Box), } impl FormElement { /// Get as DisplayItem if this is an Item variant pub fn as_item(&self) -> Option<&DisplayItem> { match self { - FormElement::Item(item) => Some(item), + FormElement::Item(item) => Some(item.as_ref()), _ => None, } } @@ -46,7 +46,7 @@ impl FormElement { /// Get mutable reference as DisplayItem if this is an Item variant pub fn as_item_mut(&mut self) -> Option<&mut DisplayItem> { match self { - FormElement::Item(item) => Some(item), + FormElement::Item(item) => Some(item.as_mut()), _ => None, } } @@ -54,7 +54,7 @@ impl FormElement { /// Get as FieldDefinition if this is a Field variant pub fn as_field(&self) -> Option<&FieldDefinition> { match self { - FormElement::Field(field) => Some(field), + FormElement::Field(field) => Some(field.as_ref()), _ => None, } } @@ -62,7 +62,7 @@ impl FormElement { /// Get mutable reference as FieldDefinition if this is a Field variant pub fn as_field_mut(&mut self) -> Option<&mut FieldDefinition> { match self { - FormElement::Field(field) => Some(field), + FormElement::Field(field) => Some(field.as_mut()), _ => None, } } @@ -158,12 +158,12 @@ impl<'de> Deserialize<'de> for FormElement { let item: DisplayItem = serde_json::from_value(serde_json::Value::Object(fields_map)) .map_err(de::Error::custom)?; - Ok(FormElement::Item(item)) + Ok(FormElement::Item(Box::new(item))) } else if field_types.contains(&element_type.as_str()) { let field: FieldDefinition = serde_json::from_value(serde_json::Value::Object(fields_map)) .map_err(de::Error::custom)?; - Ok(FormElement::Field(field)) + Ok(FormElement::Field(Box::new(field))) } else { Err(de::Error::custom(format!( "Unknown element type '{}'. Item types: {}. Field types: {}", @@ -300,14 +300,14 @@ impl FormDefinition { for mut item in self.items.drain(..) { // Assign order based on position to preserve insertion order item.order = element_list.len(); - element_list.push(FormElement::Item(item)); + element_list.push(FormElement::Item(Box::new(item))); } // Add fields, preserving insertion order after items for mut field in self.fields.drain(..) { // Assign order based on position to preserve insertion order field.order = element_list.len(); - element_list.push(FormElement::Field(field)); + element_list.push(FormElement::Field(Box::new(field))); } // Assign to elements (already in correct insertion order) @@ -325,10 +325,10 @@ impl FormDefinition { for element in self.elements.drain(..) { match element { FormElement::Field(field) => { - self.fields.push(field); + self.fields.push(*field); } FormElement::Item(item) => { - self.items.push(item); + self.items.push(*item); } } } @@ -436,6 +436,11 @@ pub struct FieldDefinition { /// Optional options list with value/label (can contain literal text or i18n keys) #[serde(default)] pub options: Vec, + /// Optional reference to another field whose values should filter this field's options + /// Example: options_from = "detected_languages" means only show options whose values + /// are present in the detected_languages field's selected values + #[serde(default)] + pub options_from: Option, /// Optional field requirement flag pub required: Option, /// Optional file extension (for editor) diff --git a/crates/typedialog-core/src/helpers.rs b/crates/typedialog-core/src/helpers.rs index 827e173..bde81c0 100644 --- a/crates/typedialog-core/src/helpers.rs +++ b/crates/typedialog-core/src/helpers.rs @@ -408,6 +408,7 @@ mod tests { default: None, placeholder: None, options: vec![], + options_from: None, required: None, file_extension: None, prefix_text: None, @@ -462,6 +463,7 @@ mod tests { default: None, placeholder: None, options: vec![], + options_from: None, required: None, file_extension: None, prefix_text: None, @@ -516,6 +518,7 @@ mod tests { default: None, placeholder: None, options: vec![], + options_from: None, required: None, file_extension: None, prefix_text: None, @@ -571,6 +574,7 @@ mod tests { default: None, placeholder: None, options: vec![], + options_from: None, required: None, file_extension: None, prefix_text: None, @@ -609,6 +613,7 @@ mod tests { default: None, placeholder: None, options: vec![], + options_from: None, required: None, file_extension: None, prefix_text: None, @@ -659,6 +664,7 @@ mod tests { default: None, placeholder: None, options: vec![], + options_from: None, required: None, file_extension: None, prefix_text: None, diff --git a/crates/typedialog-core/src/nickel/toml_generator.rs b/crates/typedialog-core/src/nickel/toml_generator.rs index 093207f..ae06312 100644 --- a/crates/typedialog-core/src/nickel/toml_generator.rs +++ b/crates/typedialog-core/src/nickel/toml_generator.rs @@ -316,6 +316,7 @@ impl TomlGenerator { default, placeholder: None, options, + options_from: None, required, file_extension: None, prefix_text: None, @@ -474,6 +475,7 @@ impl TomlGenerator { default: None, placeholder: None, options: Vec::new(), + options_from: None, required: Some(!field.optional), file_extension: None, prefix_text: None, diff --git a/crates/typedialog-core/tests/encryption_integration.rs b/crates/typedialog-core/tests/encryption_integration.rs index 5ba6f15..1c048a1 100644 --- a/crates/typedialog-core/tests/encryption_integration.rs +++ b/crates/typedialog-core/tests/encryption_integration.rs @@ -24,6 +24,7 @@ mod encryption_tests { default: None, placeholder: None, options: vec![], + options_from: None, required: None, file_extension: None, prefix_text: None, @@ -128,6 +129,7 @@ mod encryption_tests { default: None, placeholder: None, options: vec![], + options_from: None, required: None, file_extension: None, prefix_text: None, @@ -185,6 +187,7 @@ mod encryption_tests { default: None, placeholder: None, options: vec![], + options_from: None, required: None, file_extension: None, prefix_text: None, @@ -241,6 +244,7 @@ mod encryption_tests { default: None, placeholder: None, options: vec![], + options_from: None, required: None, file_extension: None, prefix_text: None, @@ -335,6 +339,7 @@ mod age_roundtrip_tests { default: None, placeholder: None, options: vec![], + options_from: None, required: None, file_extension: None, prefix_text: None, diff --git a/crates/typedialog-core/tests/nickel_integration.rs b/crates/typedialog-core/tests/nickel_integration.rs index 39a3994..97bebb2 100644 --- a/crates/typedialog-core/tests/nickel_integration.rs +++ b/crates/typedialog-core/tests/nickel_integration.rs @@ -1200,6 +1200,7 @@ fn test_encryption_roundtrip_with_redaction() { default: None, placeholder: None, options: vec![], + options_from: None, required: None, file_extension: None, prefix_text: None, @@ -1238,6 +1239,7 @@ fn test_encryption_roundtrip_with_redaction() { default: None, placeholder: None, options: vec![], + options_from: None, required: None, file_extension: None, prefix_text: None, @@ -1276,6 +1278,7 @@ fn test_encryption_roundtrip_with_redaction() { default: None, placeholder: None, options: vec![], + options_from: None, required: None, file_extension: None, prefix_text: None, @@ -1352,6 +1355,7 @@ fn test_encryption_auto_detection_from_field_type() { default: None, placeholder: None, options: vec![], + options_from: None, required: None, file_extension: None, prefix_text: None, @@ -1416,6 +1420,7 @@ fn test_sensitive_field_explicit_override() { default: None, placeholder: None, options: vec![], + options_from: None, required: None, file_extension: None, prefix_text: None, @@ -1486,6 +1491,7 @@ fn test_mixed_sensitive_and_non_sensitive_fields() { default: None, placeholder: None, options: vec![], + options_from: None, required: None, file_extension: None, prefix_text: None, diff --git a/crates/typedialog-web/src/main.rs b/crates/typedialog-web/src/main.rs index 6d6c825..841904a 100644 --- a/crates/typedialog-web/src/main.rs +++ b/crates/typedialog-web/src/main.rs @@ -312,7 +312,7 @@ fn load_nickel_defaults( let form_fields: Vec = form_elements .iter() .filter_map(|elem| match elem { - form_parser::FormElement::Field(f) => Some(f.clone()), + form_parser::FormElement::Field(f) => Some(f.as_ref().clone()), _ => None, }) .collect(); @@ -393,7 +393,7 @@ async fn execute_form( .elements .iter() .filter_map(|elem| match elem { - form_parser::FormElement::Field(f) => Some(f.clone()), + form_parser::FormElement::Field(f) => Some(f.as_ref().clone()), _ => None, }) .collect(); @@ -427,7 +427,7 @@ async fn execute_form( .elements .iter() .filter_map(|elem| match elem { - form_parser::FormElement::Field(f) => Some(f.clone()), + form_parser::FormElement::Field(f) => Some(f.as_ref().clone()), _ => None, }) .collect();