feat(tahuantinsuyu): modal "Guardar como…" real para cartas libres (F3)
Reemplaza el `save_free_chart_quick` MVP de la fase A por un
modal completo que el usuario controla:
Tree:
- Nuevo `Modal::SaveFreeChart { source_id, name, new_contact_name,
selected_contact, all_contacts, error }`.
- `open_save_free_chart_modal` abre el modal pre-poblando `name`
con el label de la carta libre y `selected_contact` con el
primer contacto existente (o `None` = nuevo contacto si no
hay ninguno).
- `gather_all_contacts` recorre la jerarquía recursivamente
devolviendo `(ContactId, "Grupo / Subgrupo / Contacto")` —
el usuario ve la ruta completa, no solo el nombre.
- `render_save_free_chart` pinta:
* Input "Nombre" pre-cargado
* Lista de contactos como botones radio (● / ○) + opción
"Nuevo contacto…" al final
* Si "Nuevo contacto…" seleccionado, aparece input
"Nombre del contacto nuevo"
* Botones Cancelar / Guardar
- `set_save_modal_contact` alterna el radio sin recrear inputs.
- Validaciones: nombre de carta no vacío; si `selected_contact`
es `None`, exigir `new_contact_name` no vacío. Errores se
muestran en una pill destructiva dentro del modal.
- Submit emite nuevo evento `TreeEvent::FreeChartSaveConfirmed
{ source_id, chart_name, contact, new_contact_name }`.
Shell:
- `persist_free_chart` resuelve el contacto destino (existente
o crea uno nuevo), llama `store.create_chart`, y al éxito
remueve la carta libre del mapa (salvo `sky-now`, que es
persistente). Si la carta libre estaba seleccionada, vuelve
al Cielo. Refresca opciones del picker para que el dropdown
ChartPicker incluya la carta recién guardada.
- El handler `SaveFreeChartRequested` queda como hook vacío;
el menú del tree abre el modal directamente con `window`.
10 tests verdes (no se afectaron los paths probados).
Próximo: F2 (editor inline de fecha/lugar/hora de la carta
libre) y F4 (botón "Guardar como…" en cada módulo overlay
con sufijo automático).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -481,12 +481,25 @@ impl Shell {
|
||||
self.apply_selection(TreeSelection::FreeChart(id), cx);
|
||||
return;
|
||||
}
|
||||
TreeEvent::SaveFreeChartRequested(id) => {
|
||||
// Por ahora: log + persistencia simple en "General"
|
||||
// (contact nuevo con el label de la carta). En la
|
||||
// fase B se reemplaza por un modal con dropdown de
|
||||
// contacto y custom name.
|
||||
self.save_free_chart_quick(id.clone());
|
||||
TreeEvent::SaveFreeChartRequested(_id) => {
|
||||
// El menú del tree abre el modal directamente; este
|
||||
// evento queda como hook por si una tecla u otra
|
||||
// UI quiere disparar el flujo sin pasar por el menú.
|
||||
return;
|
||||
}
|
||||
TreeEvent::FreeChartSaveConfirmed {
|
||||
source_id,
|
||||
chart_name,
|
||||
contact,
|
||||
new_contact_name,
|
||||
} => {
|
||||
self.persist_free_chart(
|
||||
source_id.clone(),
|
||||
chart_name.clone(),
|
||||
contact.clone(),
|
||||
new_contact_name.clone(),
|
||||
cx,
|
||||
);
|
||||
return;
|
||||
}
|
||||
TreeEvent::DeleteFreeChartRequested(id) => {
|
||||
@@ -510,40 +523,70 @@ impl Shell {
|
||||
self.apply_selection(selection.clone(), cx);
|
||||
}
|
||||
|
||||
/// Guardado rápido de una carta libre como carta natal bajo un
|
||||
/// contacto nuevo (auto-nombrado con el label de la carta libre).
|
||||
/// Es la versión MVP — en la fase B se reemplaza por un modal
|
||||
/// con dropdown de contacto + input de nombre custom.
|
||||
fn save_free_chart_quick(&mut self, id: FreeChartId) {
|
||||
let Some(chart) = self.free_charts.get(&id).cloned() else {
|
||||
/// Persiste una carta libre como `Chart` en la store. El usuario
|
||||
/// eligió en el modal: nombre + contacto destino (existente o
|
||||
/// uno nuevo creado al vuelo). La carta libre se REMUEVE del
|
||||
/// mapa tras el persist exitoso — si quedaba seleccionada,
|
||||
/// volvemos a "Cielo ahora". Si falla la persistencia, la carta
|
||||
/// libre se conserva y logueamos.
|
||||
fn persist_free_chart(
|
||||
&mut self,
|
||||
source_id: FreeChartId,
|
||||
chart_name: String,
|
||||
contact: Option<ContactId>,
|
||||
new_contact_name: Option<String>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(chart) = self.free_charts.get(&source_id).cloned() else {
|
||||
return;
|
||||
};
|
||||
// 1) Crear o reusar un contacto. Por simplicidad, creamos uno
|
||||
// nuevo con el label de la carta como nombre. La fase B le
|
||||
// dará al usuario el dropdown.
|
||||
let contact = match self.store.create_contact(None, &chart.label, None) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
eprintln!("[shell] create_contact al guardar libre: {}", e);
|
||||
// 1) Resolver el contact destino (existente o crear nuevo).
|
||||
let contact_id = match (contact, new_contact_name) {
|
||||
(Some(cid), _) => cid,
|
||||
(None, Some(name)) => match self.store.create_contact(None, &name, None) {
|
||||
Ok(c) => c.id,
|
||||
Err(e) => {
|
||||
eprintln!("[shell] create_contact al guardar libre: {}", e);
|
||||
return;
|
||||
}
|
||||
},
|
||||
(None, None) => {
|
||||
eprintln!("[shell] persist_free_chart sin contacto ni nombre nuevo");
|
||||
return;
|
||||
}
|
||||
};
|
||||
// 2) Crear la carta bajo ese contacto.
|
||||
if let Err(e) = self.store.create_chart(
|
||||
contact.id,
|
||||
// 2) Crear la carta.
|
||||
match self.store.create_chart(
|
||||
contact_id,
|
||||
chart.kind,
|
||||
&chart.label,
|
||||
&chart_name,
|
||||
&chart.birth_data,
|
||||
&chart.config,
|
||||
chart.related_chart_id,
|
||||
) {
|
||||
eprintln!("[shell] create_chart al guardar libre: {}", e);
|
||||
return;
|
||||
Ok(_) => {
|
||||
eprintln!(
|
||||
"[shell] carta libre {:?} guardada como '{}' bajo contacto {}",
|
||||
source_id, chart_name, contact_id
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("[shell] create_chart al guardar libre: {}", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
eprintln!(
|
||||
"[shell] carta libre '{}' guardada bajo contacto '{}' (id {})",
|
||||
chart.label, contact.name, contact.id
|
||||
);
|
||||
// 3) Sky-now se conserva (siempre es); las demás se quitan
|
||||
// del mapa libre. Si era la activa, volver al Cielo.
|
||||
if !source_id.is_sky_now() {
|
||||
self.free_charts.remove(&source_id);
|
||||
self.push_free_charts_to_tree(cx);
|
||||
// Si la activa era esta libre, regresar al Cielo.
|
||||
self.apply_selection(
|
||||
TreeSelection::FreeChart(FreeChartId::sky_now()),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
self.refresh_chart_options(cx);
|
||||
}
|
||||
|
||||
fn apply_selection(&mut self, sel: TreeSelection, cx: &mut Context<Self>) {
|
||||
|
||||
Reference in New Issue
Block a user