chore: fix multiselect change and other selector values, fix defaults on them
This commit is contained in:
parent
f4d3a6472b
commit
25e779a390
@ -535,11 +535,102 @@ async fn index_handler(State(state): State<Arc<WebFormState>>) -> 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<Arc<WebFormState>>) -> 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#"<div class="field" style="margin-bottom: 15px;">
|
||||
<label style="display: block; margin-bottom: 5px; font-weight: bold;">{}</label>
|
||||
<select name="{}" style="width: 100%; padding: 8px; background: #1e1e1e; color: #d4d4d4; border: 1px solid #3e3e42; box-sizing: border-box;" {}>
|
||||
<select name="{}" data-fieldname="{}" {} style="width: 100%; padding: 8px; background: #1e1e1e; color: #d4d4d4; border: 1px solid #3e3e42; box-sizing: border-box;" {}>
|
||||
<option value="">-- Select --</option>
|
||||
{}
|
||||
</select>
|
||||
</div>"#,
|
||||
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#"<select class="field-input" data-fieldname="{}"{} style="width: 100%; padding: 8px; background: #252526; color: #d4d4d4; border: 1px solid #3e3e42; border-radius: 3px;">"#,
|
||||
r#"<select class="field-input" data-fieldname="{}"{}{} style="width: 100%; padding: 8px; background: #252526; color: #d4d4d4; border: 1px solid #3e3e42; border-radius: 3px;">"#,
|
||||
field.name,
|
||||
required_attr
|
||||
required_attr,
|
||||
options_from_attr
|
||||
));
|
||||
for opt in &field.options {
|
||||
html.push_str(&format!(
|
||||
|
||||
@ -188,6 +188,7 @@ mod tests {
|
||||
default: None,
|
||||
placeholder: None,
|
||||
options: Vec::new(),
|
||||
options_from: None,
|
||||
required: None,
|
||||
file_extension: None,
|
||||
prefix_text: None,
|
||||
|
||||
@ -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<String> {
|
||||
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<String> {
|
||||
.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<String, serde_json::Value>) -> 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<String, serde_json:
|
||||
let left = condition[..pos].trim();
|
||||
let right = condition[pos + op_str.len()..].trim();
|
||||
|
||||
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),
|
||||
_ => {}
|
||||
"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));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<String, serde_json::Value>,
|
||||
) -> Vec<super::types::SelectOption> {
|
||||
// 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<String> = 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::<Vec<_>>();
|
||||
@ -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,
|
||||
})
|
||||
|
||||
@ -30,15 +30,15 @@ where
|
||||
/// Public enum for unified form structure
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum FormElement {
|
||||
Item(DisplayItem),
|
||||
Field(FieldDefinition),
|
||||
Item(Box<DisplayItem>),
|
||||
Field(Box<FieldDefinition>),
|
||||
}
|
||||
|
||||
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<SelectOption>,
|
||||
/// 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<String>,
|
||||
/// Optional field requirement flag
|
||||
pub required: Option<bool>,
|
||||
/// Optional file extension (for editor)
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -312,7 +312,7 @@ fn load_nickel_defaults(
|
||||
let form_fields: Vec<form_parser::FieldDefinition> = 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();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user