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 {
|
entries.push(FreeChartEntry {
|
||||||
id: FreeChartId::sky_now(),
|
id: FreeChartId::sky_now(),
|
||||||
label: c.label.clone(),
|
label: c.label.clone(),
|
||||||
|
birth_data: c.birth_data.clone(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
let mut others: Vec<(&FreeChartId, &Chart)> = self
|
let mut others: Vec<(&FreeChartId, &Chart)> = self
|
||||||
@@ -261,6 +262,7 @@ impl Shell {
|
|||||||
entries.push(FreeChartEntry {
|
entries.push(FreeChartEntry {
|
||||||
id: id.clone(),
|
id: id.clone(),
|
||||||
label: c.label.clone(),
|
label: c.label.clone(),
|
||||||
|
birth_data: c.birth_data.clone(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
self.tree
|
self.tree
|
||||||
@@ -502,6 +504,32 @@ impl Shell {
|
|||||||
);
|
);
|
||||||
return;
|
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) => {
|
TreeEvent::DeleteFreeChartRequested(id) => {
|
||||||
if id.is_sky_now() {
|
if id.is_sky_now() {
|
||||||
return; // no se borra el Cielo
|
return; // no se borra el Cielo
|
||||||
|
|||||||
@@ -79,6 +79,13 @@ pub enum TreeEvent {
|
|||||||
contact: Option<ContactId>,
|
contact: Option<ContactId>,
|
||||||
new_contact_name: Option<String>,
|
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,
|
form: ChartForm,
|
||||||
error: Option<SharedString>,
|
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
|
/// Guardar una carta libre como `Chart` persistido. El usuario
|
||||||
/// elige nombre + contacto destino (existente de la lista o
|
/// elige nombre + contacto destino (existente de la lista o
|
||||||
/// uno nuevo creado al vuelo). El shell escucha
|
/// uno nuevo creado al vuelo). El shell escucha
|
||||||
@@ -227,13 +243,15 @@ pub struct TahuantinsuyuTree {
|
|||||||
free_charts: Vec<FreeChartEntry>,
|
free_charts: Vec<FreeChartEntry>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Entrada de la sección "Cartas libres" — id + label visible. La
|
/// Entrada de la sección "Cartas libres" — id + label visible +
|
||||||
/// estructura del Chart en sí vive en el shell (`free_charts` de
|
/// birth_data clonado (para pre-poblar el modal "Editar datos…").
|
||||||
/// `Shell`), no en el tree.
|
/// 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)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct FreeChartEntry {
|
pub struct FreeChartEntry {
|
||||||
pub id: FreeChartId,
|
pub id: FreeChartId,
|
||||||
pub label: String,
|
pub label: String,
|
||||||
|
pub birth_data: StoredBirthData,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Preset de ciudad con datos canónicos para autocompletar lat/lon/tz
|
/// 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() {
|
let form = match self.modal.as_mut() {
|
||||||
Some(Modal::CreateChart { form, .. }) => form,
|
Some(Modal::CreateChart { form, .. }) => form,
|
||||||
Some(Modal::EditChart { form, .. }) => form,
|
Some(Modal::EditChart { form, .. }) => form,
|
||||||
|
Some(Modal::EditFreeChart { form, .. }) => form,
|
||||||
_ => {
|
_ => {
|
||||||
self.city_picker_open = false;
|
self.city_picker_open = false;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
@@ -878,6 +897,58 @@ impl TahuantinsuyuTree {
|
|||||||
/// contactos es un snapshot recursivo de toda la jerarquía
|
/// contactos es un snapshot recursivo de toda la jerarquía
|
||||||
/// (no solo el nivel raíz). El usuario elige uno existente o
|
/// (no solo el nivel raíz). El usuario elige uno existente o
|
||||||
/// deja en "Nuevo contacto" para que se cree uno al confirmar.
|
/// 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
|
/// Cambia `selected_contact` del modal `SaveFreeChart` activo
|
||||||
/// sin recrear los inputs. Permite alternar entre los botones
|
/// sin recrear los inputs. Permite alternar entre los botones
|
||||||
/// radio "contacto existente" y "Nuevo contacto…".
|
/// 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 {
|
Modal::SaveFreeChart {
|
||||||
source_id,
|
source_id,
|
||||||
name,
|
name,
|
||||||
@@ -1617,6 +1714,14 @@ impl TahuantinsuyuTree {
|
|||||||
}
|
}
|
||||||
MenuTarget::FreeChart(fid) => {
|
MenuTarget::FreeChart(fid) => {
|
||||||
let is_sky = fid.is_sky_now();
|
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();
|
let fid_save = fid.clone();
|
||||||
items = items.child(
|
items = items.child(
|
||||||
menu_item("tts-menu-save-free", "Guardar como…", theme).on_click(
|
menu_item("tts-menu-save-free", "Guardar como…", theme).on_click(
|
||||||
@@ -1708,6 +1813,15 @@ impl TahuantinsuyuTree {
|
|||||||
&self.city_atlas,
|
&self.city_atlas,
|
||||||
cx,
|
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 {
|
Modal::SaveFreeChart {
|
||||||
source_id,
|
source_id,
|
||||||
name,
|
name,
|
||||||
|
|||||||
Reference in New Issue
Block a user