feat(main): wire 0011 — context menu + triggers + sync de windows

- Context menu del viewport detecta type_ref='Table' y anade Expand/
  Collapse table. Toggle escribe metadata.expanded en BD y resincroniza
  table_windows.
- Triggers want_promote_row -> tableview_promote_row + reload + focus
  inspector con la entidad recien creada.
- want_demote_entity -> tableview_demote_row + reload.
- want_focus_entity: resuelve entity_id -> node_idx via FNV1a, centra
  camara, abre inspector.
- want_import -> tableview_ingest_file + tableview_create + reload.
- Loop por table_windows page_dirty -> tableview_count + (si columns
  vacios) descubre+persiste columnas + tableview_page.
- Cierre via X de ventana detectado leyendo open=false; bajamos
  expanded en BD y borramos del mapa.
- Sync de table_windows tras load_input y reload_after_mutation.
- views_table_window + views_import_dataset_modal llamados en render().
This commit is contained in:
2026-05-01 01:53:12 +02:00
parent cedfe3b616
commit 6c1be87a2d
+159
View File
@@ -264,6 +264,9 @@ static bool load_input() {
std::fprintf(stdout,
"[graph_explorer] table counts refreshed: %zu tables, %lld total rows\n",
g_app.table_node_counts.size(), (long long)total_rows);
// Sync de windows expandidas (issue 0011) — reabre las que el
// usuario tenia abiertas en la sesion previa (metadata.expanded=true).
ge::views_table_windows_sync(g_app, g_input.uri);
}
// Cache de la vista tabla (issue 0004) — pull bulk + neighbors desde grafo.
@@ -420,6 +423,28 @@ static void render_context_menu() {
ImGui::TextDisabled("%s", lbl && *lbl ? lbl : "(unnamed)");
ImGui::Separator();
// Detectar si el nodo es Table y resolver entity_id para opciones tabla.
bool is_table = false;
if (n.type_id < (uint16_t)g_graph.type_count) {
const EntityType& t = g_graph.types[n.type_id];
if (t.name && std::strcmp(t.name, "Table") == 0) is_table = true;
}
const char* sql_id = ge::entity_index_lookup(g_idx, n.user_data);
if (is_table && sql_id) {
// Determinar estado expanded actual sin ir a BD: mira table_windows.
bool currently_expanded =
g_app.table_windows.find(sql_id) != g_app.table_windows.end();
const char* lbl_exp = currently_expanded
? TI_X " Collapse table"
: TI_TABLE " Expand table";
if (ImGui::MenuItem(lbl_exp)) {
g_app.want_toggle_expanded = true;
g_app.toggle_expanded_id = sql_id;
}
ImGui::Separator();
}
if (ImGui::BeginMenu("Change type")) {
// Construye un set ordenado y deduplicado: tipos del grafo + defaults.
// Asi evitamos colisiones de ID en ImGui ("person" en grafo y default).
@@ -614,6 +639,7 @@ static void render() {
load_input();
}
// ---- Type Editor (issue 0007) ----
if (g_app.want_types_save) {
g_app.want_types_save = false;
@@ -705,6 +731,9 @@ static void render() {
// Refresh Table node counts (issue 0010).
ge::tableview_refresh_counts(g_input.uri, &g_app.table_node_counts);
// Sincroniza windows (issue 0011) por si una Table aparecio o desaparecio.
ge::views_table_windows_sync(g_app, g_input.uri);
// Refresh table cache (issue 0004).
std::vector<ge::EntityRowSnapshot> snap;
if (ge::entity_list_rows(g_input.uri, &snap)) {
@@ -802,6 +831,132 @@ static void render() {
g_app.want_change_type = false;
}
// ---- Table node UI fase 2 (issue 0011) ----
if (g_app.want_toggle_expanded && !g_app.toggle_expanded_id.empty()
&& !g_input_path.empty()) {
std::string id = g_app.toggle_expanded_id;
bool currently = g_app.table_windows.find(id) != g_app.table_windows.end();
ge::tableview_set_expanded(g_input_path.c_str(), id.c_str(), !currently);
ge::views_table_windows_sync(g_app, g_input_path.c_str());
g_app.want_toggle_expanded = false;
g_app.toggle_expanded_id.clear();
}
// Cierre via X de la ventana -> bajar expanded en BD.
for (auto it = g_app.table_windows.begin(); it != g_app.table_windows.end(); ) {
if (!it->second.open && !g_input_path.empty()) {
ge::tableview_set_expanded(g_input_path.c_str(),
it->first.c_str(), false);
it = g_app.table_windows.erase(it);
} else ++it;
}
// Refrescar la pagina si alguna window esta dirty.
for (auto& kv : g_app.table_windows) {
auto& w = kv.second;
if (!w.page_dirty) continue;
const auto& m = w.meta;
ge::tableview_count(m.duckdb_path_abs.c_str(), m.table_name.c_str(),
m.filter_sql.empty() ? nullptr : m.filter_sql.c_str(),
&w.total_rows);
if (m.columns.empty()) {
std::vector<std::string> cols;
if (ge::tableview_list_columns(m.duckdb_path_abs.c_str(),
m.table_name.c_str(), &cols)) {
ge::tableview_set_columns(g_input_path.c_str(),
m.entity_id.c_str(), cols);
w.meta.columns = cols;
}
}
ge::tableview_page(m.duckdb_path_abs.c_str(), m.table_name.c_str(),
m.id_column.c_str(), w.meta.columns,
m.filter_sql.empty() ? nullptr : m.filter_sql.c_str(),
g_input_path.c_str(), m.row_type.c_str(),
w.offset, 200, &w.page);
w.page_dirty = false;
}
if (g_app.want_promote_row && !g_app.promote_table_id.empty()
&& !g_input_path.empty()) {
ge::TableMetadata m;
if (ge::tableview_get_metadata(g_input_path.c_str(),
g_app.promote_table_id.c_str(), &m)) {
char new_id[128] = {};
if (ge::tableview_promote_row(g_input_path.c_str(),
m.duckdb_path_abs.c_str(),
m.table_name.c_str(),
g_app.promote_row_id.c_str(),
m.row_type.c_str(),
m.label_column.c_str(),
new_id, sizeof(new_id))) {
std::fprintf(stdout, "[promote] %s -> %s\n",
g_app.promote_row_id.c_str(), new_id);
auto it = g_app.table_windows.find(g_app.promote_table_id);
if (it != g_app.table_windows.end()) it->second.page_dirty = true;
reload_after_mutation();
g_app.want_focus_entity = true;
g_app.focus_entity_id = new_id;
}
}
g_app.want_promote_row = false;
g_app.promote_table_id.clear();
g_app.promote_row_id.clear();
}
if (g_app.want_demote_entity && !g_app.demote_entity_id.empty()
&& !g_input_path.empty()) {
if (ge::tableview_demote_row(g_input_path.c_str(),
g_app.demote_entity_id.c_str())) {
std::fprintf(stdout, "[demote] %s\n", g_app.demote_entity_id.c_str());
for (auto& kv : g_app.table_windows) kv.second.page_dirty = true;
reload_after_mutation();
}
g_app.want_demote_entity = false;
g_app.demote_entity_id.clear();
}
if (g_app.want_focus_entity && !g_app.focus_entity_id.empty()) {
for (int i = 0; i < g_graph.node_count; ++i) {
const char* sid = ge::entity_index_lookup(
g_idx, g_graph.nodes[i].user_data);
if (sid && g_app.focus_entity_id == sid) {
g_app.filter_focus_target = i;
graph_viewport_clear_selection(g_graph, g_viewport);
graph_viewport_add_to_selection(g_graph, g_viewport, i);
g_app.panel_inspector = true;
ge::views_inspector_load_draft(g_app, i, sid);
g_app.insp_node_idx = i;
g_app.insp_entity_id = sid;
break;
}
}
g_app.want_focus_entity = false;
g_app.focus_entity_id.clear();
}
if (g_app.want_import) {
g_app.want_import = false;
g_app.import_error.clear();
std::string duck_abs = ge::tableview_resolve_path(
g_input_path.c_str(), g_app.import_duckdb_buf);
std::string err;
if (!ge::tableview_ingest_file(duck_abs.c_str(),
g_app.import_path_buf,
g_app.import_table_buf,
ge::INGEST_AUTO, &err)) {
g_app.import_error = "Ingest failed: " + err;
} else {
char new_id[80] = {};
if (ge::tableview_create(g_input_path.c_str(),
g_app.import_table_buf,
g_app.import_duckdb_buf,
g_app.import_table_buf,
g_app.import_row_type_buf,
new_id, sizeof(new_id))) {
std::fprintf(stdout, "[import] %s -> %s\n",
g_app.import_path_buf, new_id);
g_app.show_import_modal = false;
reload_after_mutation();
} else {
g_app.import_error = "Tabla DuckDB creada pero no se pudo registrar el nodo.";
}
}
}
// ---- Inspector (issue 0008): sync draft con seleccion + save/discard ----
{
const auto& sel = g_viewport.selection;
@@ -1001,6 +1156,10 @@ static void render() {
ImGui::SetNextWindowSize(ImVec2(820.0f, 520.0f), ImGuiCond_FirstUseEver);
ge::views_table(g_app);
// Table node windows (issue 0011) — una por Table expandida.
ge::views_table_window(g_app);
ge::views_import_dataset_modal(g_app);
g_first_render = false;
}