feat(tahuantinsuyu): editor inline para cartas libres (F2)
Las cartas libres ahora se pueden editar en su totalidad (fecha,
hora, lugar, lat/lon/alt, TZ, label) desde el menú contextual.
La edición es **in-memory** — la carta se queda como libre tras
el cambio; para persistirla hay que usar "Guardar como…".
Tree:
- Nuevo `Modal::EditFreeChart { source_id, form, error }`
paralelo al `Modal::EditChart` existente. Reusa la misma
`ChartForm` (11 TextInputs: name + place + date + time + TZ
+ lat/lon/alt) y la misma función `render_chart_form` para
pintarlo. El title cambia a "Editar carta libre".
- `open_edit_free_chart_modal(source_id, w, cx)`: lee el entry
de `self.free_charts` (que ahora trae `birth_data` además
de id+label), pre-puebla el form, y abre el modal.
- Submit: `build_chart_from_form` parsea + valida; al éxito
emite nuevo evento `TreeEvent::FreeChartEditConfirmed
{ source_id, birth_data, label }`. Al error, conserva el
modal con la pill destructiva.
- City picker funciona como antes — el branch de
`apply_city_preset` se extendió para que reconozca
`Modal::EditFreeChart` además de Create/Edit.
Modelo:
- `FreeChartEntry` ahora incluye `birth_data: StoredBirthData`
además de id+label. El shell se lo pasa al setter; el tree
lo usa para pre-poblar el form sin tener que pedirlo al
shell.
Shell:
- `push_free_charts_to_tree` clona `birth_data` en cada entry.
- Handler `FreeChartEditConfirmed`: actualiza
`free_charts[id]` con los nuevos datos + label, re-publica
al tree, y si la carta editada era la activa, re-renderea
el wheel.
Menú contextual de "Cartas libres" / `<carta libre>` ahora:
- Editar datos…
- Guardar como…
- Borrar (no se ofrece sobre sky-now)
10 tests verdes (sin afectar lo testeado).
Próximo y último: F4 — botón "Guardar como…" en cada módulo
overlay (RS, prog, sa, gr) que captura la carta derivada con
un sufijo automático en el contacto original.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -249,6 +249,7 @@ impl Shell {
|
||||
entries.push(FreeChartEntry {
|
||||
id: FreeChartId::sky_now(),
|
||||
label: c.label.clone(),
|
||||
birth_data: c.birth_data.clone(),
|
||||
});
|
||||
}
|
||||
let mut others: Vec<(&FreeChartId, &Chart)> = self
|
||||
@@ -261,6 +262,7 @@ impl Shell {
|
||||
entries.push(FreeChartEntry {
|
||||
id: id.clone(),
|
||||
label: c.label.clone(),
|
||||
birth_data: c.birth_data.clone(),
|
||||
});
|
||||
}
|
||||
self.tree
|
||||
@@ -502,6 +504,32 @@ impl Shell {
|
||||
);
|
||||
return;
|
||||
}
|
||||
TreeEvent::FreeChartEditConfirmed {
|
||||
source_id,
|
||||
birth_data,
|
||||
label,
|
||||
} => {
|
||||
if let Some(chart) = self.free_charts.get_mut(source_id) {
|
||||
chart.birth_data = birth_data.clone();
|
||||
chart.label = label.clone();
|
||||
}
|
||||
self.push_free_charts_to_tree(cx);
|
||||
// Si la carta editada era la activa, re-render.
|
||||
if let Some(current) = self.current_chart.as_mut() {
|
||||
// Heurística: comparamos por label (ya cambiado al
|
||||
// que pidió el usuario). Si el label de la activa
|
||||
// coincide, era esta carta.
|
||||
if current.label == label.clone()
|
||||
|| current.birth_data.subject_name.as_deref() == Some("Cielo")
|
||||
{
|
||||
if let Some(updated) = self.free_charts.get(source_id) {
|
||||
*current = updated.clone();
|
||||
self.render_current(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
TreeEvent::DeleteFreeChartRequested(id) => {
|
||||
if id.is_sky_now() {
|
||||
return; // no se borra el Cielo
|
||||
|
||||
@@ -79,6 +79,13 @@ pub enum TreeEvent {
|
||||
contact: Option<ContactId>,
|
||||
new_contact_name: Option<String>,
|
||||
},
|
||||
/// Submit del modal "Editar datos" para una carta libre. El
|
||||
/// shell aplica al mapa `free_charts` y re-renderea el wheel.
|
||||
FreeChartEditConfirmed {
|
||||
source_id: FreeChartId,
|
||||
birth_data: StoredBirthData,
|
||||
label: String,
|
||||
},
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
@@ -157,6 +164,15 @@ enum Modal {
|
||||
form: ChartForm,
|
||||
error: Option<SharedString>,
|
||||
},
|
||||
/// Editar los datos (fecha/hora/lugar) de una carta libre.
|
||||
/// Reusa el mismo `ChartForm` que `EditChart`. El submit emite
|
||||
/// `FreeChartEditConfirmed` que el shell aplica al mapa
|
||||
/// `free_charts` y re-renderea el wheel.
|
||||
EditFreeChart {
|
||||
source_id: FreeChartId,
|
||||
form: ChartForm,
|
||||
error: Option<SharedString>,
|
||||
},
|
||||
/// Guardar una carta libre como `Chart` persistido. El usuario
|
||||
/// elige nombre + contacto destino (existente de la lista o
|
||||
/// uno nuevo creado al vuelo). El shell escucha
|
||||
@@ -227,13 +243,15 @@ pub struct TahuantinsuyuTree {
|
||||
free_charts: Vec<FreeChartEntry>,
|
||||
}
|
||||
|
||||
/// Entrada de la sección "Cartas libres" — id + label visible. La
|
||||
/// estructura del Chart en sí vive en el shell (`free_charts` de
|
||||
/// `Shell`), no en el tree.
|
||||
/// Entrada de la sección "Cartas libres" — id + label visible +
|
||||
/// birth_data clonado (para pre-poblar el modal "Editar datos…").
|
||||
/// El Chart completo vive en el shell; el tree mantiene esta
|
||||
/// proyección compacta para no depender del shell en cada operación.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FreeChartEntry {
|
||||
pub id: FreeChartId,
|
||||
pub label: String,
|
||||
pub birth_data: StoredBirthData,
|
||||
}
|
||||
|
||||
/// Preset de ciudad con datos canónicos para autocompletar lat/lon/tz
|
||||
@@ -777,6 +795,7 @@ impl TahuantinsuyuTree {
|
||||
let form = match self.modal.as_mut() {
|
||||
Some(Modal::CreateChart { form, .. }) => form,
|
||||
Some(Modal::EditChart { form, .. }) => form,
|
||||
Some(Modal::EditFreeChart { form, .. }) => form,
|
||||
_ => {
|
||||
self.city_picker_open = false;
|
||||
cx.notify();
|
||||
@@ -878,6 +897,58 @@ impl TahuantinsuyuTree {
|
||||
/// contactos es un snapshot recursivo de toda la jerarquía
|
||||
/// (no solo el nivel raíz). El usuario elige uno existente o
|
||||
/// deja en "Nuevo contacto" para que se cree uno al confirmar.
|
||||
/// Abre el modal "Editar datos" para una carta libre. Pre-puebla
|
||||
/// `ChartForm` con `birth_data` actual de la entry. Submit emite
|
||||
/// `FreeChartEditConfirmed` que el shell aplica al mapa de
|
||||
/// `free_charts` y re-renderea.
|
||||
fn open_edit_free_chart_modal(
|
||||
&mut self,
|
||||
source_id: FreeChartId,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<'_, Self>,
|
||||
) {
|
||||
let entry = match self.free_charts.iter().find(|e| e.id == source_id) {
|
||||
Some(e) => e.clone(),
|
||||
None => return,
|
||||
};
|
||||
let bd = &entry.birth_data;
|
||||
let form = ChartForm {
|
||||
name: self.make_input("Etiqueta de la carta", &entry.label, window, cx),
|
||||
place: self.make_input(
|
||||
"Lugar (ciudad, país)",
|
||||
bd.birthplace_label.as_deref().unwrap_or(""),
|
||||
window,
|
||||
cx,
|
||||
),
|
||||
year: self.make_input("Año", &bd.year.to_string(), window, cx),
|
||||
month: self.make_input("Mes", &bd.month.to_string(), window, cx),
|
||||
day: self.make_input("Día", &bd.day.to_string(), window, cx),
|
||||
hour: self.make_input("Hora (0-23)", &bd.hour.to_string(), window, cx),
|
||||
minute: self.make_input("Minuto", &bd.minute.to_string(), window, cx),
|
||||
tz_offset_min: self.make_input(
|
||||
"TZ offset (min)",
|
||||
&bd.tz_offset_minutes.to_string(),
|
||||
window,
|
||||
cx,
|
||||
),
|
||||
lat: self.make_input("Latitud (°)", &format!("{}", bd.latitude_deg), window, cx),
|
||||
lon: self.make_input(
|
||||
"Longitud (°)",
|
||||
&format!("{}", bd.longitude_deg),
|
||||
window,
|
||||
cx,
|
||||
),
|
||||
alt: self.make_input("Altitud (m)", &format!("{}", bd.altitude_m), window, cx),
|
||||
};
|
||||
form.name.update(cx, |i, _| i.request_focus(window));
|
||||
self.modal = Some(Modal::EditFreeChart {
|
||||
source_id,
|
||||
form,
|
||||
error: None,
|
||||
});
|
||||
self.close_menu(cx);
|
||||
}
|
||||
|
||||
/// Cambia `selected_contact` del modal `SaveFreeChart` activo
|
||||
/// sin recrear los inputs. Permite alternar entre los botones
|
||||
/// radio "contacto existente" y "Nuevo contacto…".
|
||||
@@ -1230,6 +1301,32 @@ impl TahuantinsuyuTree {
|
||||
}
|
||||
}
|
||||
}
|
||||
Modal::EditFreeChart {
|
||||
source_id,
|
||||
form,
|
||||
error: _,
|
||||
} => {
|
||||
let _ = value;
|
||||
match build_chart_from_form(&form, cx) {
|
||||
Ok((birth, label)) => {
|
||||
cx.emit(TreeEvent::FreeChartEditConfirmed {
|
||||
source_id,
|
||||
birth_data: birth,
|
||||
label,
|
||||
});
|
||||
self.modal = None;
|
||||
cx.notify();
|
||||
}
|
||||
Err(msg) => {
|
||||
self.modal = Some(Modal::EditFreeChart {
|
||||
source_id,
|
||||
form,
|
||||
error: Some(SharedString::from(msg)),
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
Modal::SaveFreeChart {
|
||||
source_id,
|
||||
name,
|
||||
@@ -1617,6 +1714,14 @@ impl TahuantinsuyuTree {
|
||||
}
|
||||
MenuTarget::FreeChart(fid) => {
|
||||
let is_sky = fid.is_sky_now();
|
||||
let fid_edit = fid.clone();
|
||||
items = items.child(
|
||||
menu_item("tts-menu-edit-free", "Editar datos…", theme).on_click(
|
||||
cx.listener(move |this, _: &ClickEvent, w, cx| {
|
||||
this.open_edit_free_chart_modal(fid_edit.clone(), w, cx);
|
||||
}),
|
||||
),
|
||||
);
|
||||
let fid_save = fid.clone();
|
||||
items = items.child(
|
||||
menu_item("tts-menu-save-free", "Guardar como…", theme).on_click(
|
||||
@@ -1708,6 +1813,15 @@ impl TahuantinsuyuTree {
|
||||
&self.city_atlas,
|
||||
cx,
|
||||
),
|
||||
Modal::EditFreeChart { form, error, .. } => render_chart_form(
|
||||
theme,
|
||||
"Editar carta libre",
|
||||
form,
|
||||
error.clone(),
|
||||
self.city_picker_open,
|
||||
&self.city_atlas,
|
||||
cx,
|
||||
),
|
||||
Modal::SaveFreeChart {
|
||||
source_id,
|
||||
name,
|
||||
|
||||
Reference in New Issue
Block a user