feat(nakui): Fase 4 del ERP — listas profesionales (orden/búsqueda/página)
Las vistas de lista de meta-form ganan: orden por columna (clic en header cicla asc→desc→off con indicador ▲/▼), búsqueda en vivo (caja 🔍 que filtra por search_in mientras se teclea, vía cx.observe del TextInput) y paginación (25/página, controles ◀▶). Sin cambios de schema: son estado del widget. Helpers puros cmp_values (meta-runtime) y next_sort con tests. Tests verdes (meta-runtime 63, meta-form 8); clippy limpio. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -3,11 +3,38 @@
|
||||
//! Sin GPUI: devuelven `String`s. El widget renderer los wrap-ea
|
||||
//! en `div().child(...)` o equivalente.
|
||||
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use serde_json::Value;
|
||||
use uuid::Uuid;
|
||||
|
||||
use nahual_meta_schema::ValueFormat;
|
||||
|
||||
/// Compara dos valores de celda para ordenar una lista. `None`/`null`
|
||||
/// ordenan antes que cualquier valor. Números por valor numérico,
|
||||
/// strings case-insensitive, bools `false < true`; tipos mixtos por su
|
||||
/// forma string (orden estable, no semántico).
|
||||
pub fn cmp_values(a: Option<&Value>, b: Option<&Value>) -> Ordering {
|
||||
let nullish = |v: Option<&Value>| matches!(v, None | Some(Value::Null));
|
||||
match (nullish(a), nullish(b)) {
|
||||
(true, true) => return Ordering::Equal,
|
||||
(true, false) => return Ordering::Less,
|
||||
(false, true) => return Ordering::Greater,
|
||||
(false, false) => {}
|
||||
}
|
||||
match (a, b) {
|
||||
(Some(Value::Number(x)), Some(Value::Number(y))) => x
|
||||
.as_f64()
|
||||
.partial_cmp(&y.as_f64())
|
||||
.unwrap_or(Ordering::Equal),
|
||||
(Some(Value::String(x)), Some(Value::String(y))) => x.to_lowercase().cmp(&y.to_lowercase()),
|
||||
(Some(Value::Bool(x)), Some(Value::Bool(y))) => x.cmp(y),
|
||||
(Some(x), Some(y)) => x.to_string().cmp(&y.to_string()),
|
||||
// Inalcanzable: el chequeo nullish de arriba cubre los None.
|
||||
_ => Ordering::Equal,
|
||||
}
|
||||
}
|
||||
|
||||
/// Etiqueta humana para representar un record en el selector de
|
||||
/// EntityRef y en columnas de referencia. Heurística: prefiere campos
|
||||
/// de nombre comunes (ES + EN); fallback al UUID corto.
|
||||
@@ -208,6 +235,35 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cmp_values_orders_numbers_strings_nulls() {
|
||||
// Números por valor, no lexicográfico.
|
||||
assert_eq!(
|
||||
cmp_values(Some(&json!(2)), Some(&json!(10))),
|
||||
Ordering::Less
|
||||
);
|
||||
// Strings case-insensitive.
|
||||
assert_eq!(
|
||||
cmp_values(Some(&json!("banana")), Some(&json!("Apple"))),
|
||||
Ordering::Greater
|
||||
);
|
||||
// null/None ordena primero.
|
||||
assert_eq!(cmp_values(None, Some(&json!(1))), Ordering::Less);
|
||||
assert_eq!(
|
||||
cmp_values(Some(&Value::Null), Some(&json!("x"))),
|
||||
Ordering::Less
|
||||
);
|
||||
assert_eq!(
|
||||
cmp_values(Some(&json!(5)), Some(&json!(5))),
|
||||
Ordering::Equal
|
||||
);
|
||||
// Bools.
|
||||
assert_eq!(
|
||||
cmp_values(Some(&json!(false)), Some(&json!(true))),
|
||||
Ordering::Less
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_value_non_number_falls_back_to_render_value() {
|
||||
assert_eq!(
|
||||
|
||||
@@ -32,8 +32,8 @@ pub mod testing;
|
||||
pub use backend::{MetaBackend, WriteOutcome};
|
||||
pub use delta::{compute_clear_fields, compute_field_delta};
|
||||
pub use format::{
|
||||
format_value, human_label_for_record, preview_value, render_value, short_hash, short_uuid,
|
||||
value_to_input_text,
|
||||
cmp_values, format_value, human_label_for_record, preview_value, render_value, short_hash,
|
||||
short_uuid, value_to_input_text,
|
||||
};
|
||||
pub use parse::{infer_param_value, parse_field_value, resolve_param_value};
|
||||
pub use refs::validate_entity_refs;
|
||||
|
||||
Reference in New Issue
Block a user