diff --git a/entity_ops.cpp b/entity_ops.cpp index 5399d03..138171f 100644 --- a/entity_ops.cpp +++ b/entity_ops.cpp @@ -782,10 +782,32 @@ bool entity_list_rows(const char* db_path, if (db) sqlite3_close(db); return false; } - const char* sql = + // Detecta si existe la columna `group_id` (issue 0035a). En BDs viejas + // sin la columna, el campo queda vacio y nada cambia. + bool has_group_id = false; + { + sqlite3_stmt* pst = nullptr; + if (sqlite3_prepare_v2(db, "PRAGMA table_info(entities)", -1, &pst, nullptr) == SQLITE_OK) { + while (sqlite3_step(pst) == SQLITE_ROW) { + const unsigned char* name = sqlite3_column_text(pst, 1); + if (name && std::strcmp((const char*)name, "group_id") == 0) { + has_group_id = true; + break; + } + } + sqlite3_finalize(pst); + } + } + const char* sql_with = + "SELECT id, COALESCE(name,''), COALESCE(type_ref,''), " + " COALESCE(status,''), COALESCE(updated_at,''), " + " COALESCE(group_id,'') " + "FROM entities ORDER BY type_ref, name"; + const char* sql_without = "SELECT id, COALESCE(name,''), COALESCE(type_ref,''), " " COALESCE(status,''), COALESCE(updated_at,'') " "FROM entities ORDER BY type_ref, name"; + const char* sql = has_group_id ? sql_with : sql_without; sqlite3_stmt* st = nullptr; if (sqlite3_prepare_v2(db, sql, -1, &st, nullptr) != SQLITE_OK) { sqlite3_close(db); @@ -803,6 +825,10 @@ bool entity_list_rows(const char* db_path, r.type_ref = a2 ? (const char*)a2 : ""; r.status = a3 ? (const char*)a3 : ""; r.updated_at = a4 ? (const char*)a4 : ""; + if (has_group_id) { + const unsigned char* a5 = sqlite3_column_text(st, 5); + r.group_id = a5 ? (const char*)a5 : ""; + } out->push_back(std::move(r)); } sqlite3_finalize(st); diff --git a/entity_ops.h b/entity_ops.h index 255848c..9e25b0a 100644 --- a/entity_ops.h +++ b/entity_ops.h @@ -130,6 +130,7 @@ struct EntityRowSnapshot { std::string type_ref; std::string status; std::string updated_at; + std::string group_id; // "" si la fila no pertenece a ningun grupo }; // Carga todas las filas de `entities` ordenadas por type_ref, name. Tolera BD diff --git a/main.cpp b/main.cpp index 2371e59..e907b5f 100644 --- a/main.cpp +++ b/main.cpp @@ -729,6 +729,7 @@ static bool load_input(bool first_load) { tr.type_ref = std::move(s.type_ref); tr.status = std::move(s.status); tr.updated_at = std::move(s.updated_at); + tr.group_id = std::move(s.group_id); g_app.table_rows.push_back(std::move(tr)); } ge::views_table_refresh_indices(g_app); @@ -1558,6 +1559,7 @@ static void render() { tr.type_ref = std::move(s.type_ref); tr.status = std::move(s.status); tr.updated_at = std::move(s.updated_at); + tr.group_id = std::move(s.group_id); g_app.table_rows.push_back(std::move(tr)); } ge::views_table_refresh_indices(g_app); @@ -1855,21 +1857,40 @@ static void render() { } // Note editor — abrir / guardar. + // Excepcion (issue 0035d): si el nodo es un Group, en lugar de abrir + // Note se abre el panel Table con filtro por group_id. if (g_app.want_open_note && g_app.open_note_target >= 0 && g_app.open_note_target < g_graph.node_count) { int n = g_app.open_note_target; const char* sql_id = ge::entity_index_lookup(g_idx, g_graph.nodes[n].user_data); - if (sql_id) { + + // Detectar si el nodo es de tipo Group. + bool is_group = false; + uint16_t tid = g_graph.nodes[n].type_id; + const char* type_name = (tid < (uint16_t)g_graph.type_count + && g_graph.types[tid].name) + ? g_graph.types[tid].name : ""; + if (type_name && std::strcmp(type_name, "Group") == 0) is_group = true; + + if (is_group && sql_id) { + // Drill-in: abrir tableview filtrada por group_id = sql_id. + g_app.table_filter_group_id = sql_id; + const char* lbl = graph::graph_label(&g_graph, g_graph.nodes[n].label_idx); + g_app.table_filter_group_name = lbl ? lbl : sql_id; + // Reset filtros que pueden ocultar las filas del grupo. + g_app.table_search_buf[0] = 0; + g_app.table_col_filters.clear(); + g_app.table_show_all = true; + g_app.panel_table = true; + ImGui::SetWindowFocus("Table"); + } else if (sql_id) { std::string md; ge::entity_get_notes(g_app.input_db_path.c_str(), sql_id, &md); g_app.note_node = n; g_app.note_entity_id = sql_id; const char* lbl = graph::graph_label(&g_graph, g_graph.nodes[n].label_idx); g_app.note_entity_label = lbl ? lbl : ""; - uint16_t tid = g_graph.nodes[n].type_id; - g_app.note_entity_type = (tid < (uint16_t)g_graph.type_count - && g_graph.types[tid].name) - ? g_graph.types[tid].name : ""; + g_app.note_entity_type = type_name; // Asegura buffer >= max(64KB, contenido + holgura). size_t need = md.size() + 4096; if (need < 65536) need = 65536; diff --git a/views.cpp b/views.cpp index 1b49932..e180d1e 100644 --- a/views.cpp +++ b/views.cpp @@ -1733,12 +1733,41 @@ void render_one_table(AppState& app, std::vector& visible_indices) { } // namespace void views_table(AppState& app) { - if (!app.panel_table) return; + // Si el panel se cierra, limpiamos el filtro de grupo (RAM-only). + if (!app.panel_table) { + if (!app.table_filter_group_id.empty()) { + app.table_filter_group_id.clear(); + app.table_filter_group_name.clear(); + } + return; + } if (!ImGui::Begin("Table", &app.panel_table)) { ImGui::End(); return; } + // Breadcrumb de drill-in por grupo (issue 0035d). + if (!app.table_filter_group_id.empty()) { + // Contar filas que pertenecen a este grupo (sin aplicar otros filtros). + size_t n_group = 0; + for (const auto& r : app.table_rows) { + if (r.group_id == app.table_filter_group_id) ++n_group; + } + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.85f, 0.75f, 0.35f, 1.0f)); + ImGui::Text(TI_FOLDER " Group: %s (%zu)", + app.table_filter_group_name.empty() + ? app.table_filter_group_id.c_str() + : app.table_filter_group_name.c_str(), + n_group); + ImGui::PopStyleColor(); + ImGui::SameLine(); + if (fn_ui::button("Clear group filter", fn_ui::ButtonVariant::Subtle)) { + app.table_filter_group_id.clear(); + app.table_filter_group_name.clear(); + } + ImGui::Separator(); + } + // Toolbar superior: search + show all. ImGui::SetNextItemWidth(220); ImGui::InputTextWithHint("##tsearch", TI_SEARCH " filter name/id...", @@ -1777,12 +1806,15 @@ void views_table(AppState& app) { return; } - // Indices por tipo. + // Indices por tipo. Si hay filtro de grupo (issue 0035d) acotamos los + // types tabulables a los presentes dentro del grupo. std::vector types_present; types_present.reserve(8); { std::unordered_set seen; for (const auto& r : app.table_rows) { + if (!app.table_filter_group_id.empty() + && r.group_id != app.table_filter_group_id) continue; if (seen.insert(r.type_ref).second) types_present.push_back(r.type_ref); } std::sort(types_present.begin(), types_present.end()); @@ -1793,6 +1825,10 @@ void views_table(AppState& app) { v.reserve(app.table_rows.size()); for (size_t i = 0; i < app.table_rows.size(); ++i) { const auto& r = app.table_rows[i]; + // Drill-in por grupo (issue 0035d): si hay filtro activo, solo + // pasan filas cuyo group_id coincide. + if (!app.table_filter_group_id.empty() + && r.group_id != app.table_filter_group_id) continue; if (type_filter && r.type_ref != type_filter) continue; if (app.table_search_buf[0] && !ci_contains(r.name, app.table_search_buf) diff --git a/views.h b/views.h index a664f68..0efef1e 100644 --- a/views.h +++ b/views.h @@ -210,6 +210,7 @@ struct AppState { std::string type_ref; std::string status; std::string updated_at; + std::string group_id; // si pertenece a un Group, su sql id; "" si no int neighbors = 0; int node_idx = -1; }; @@ -225,6 +226,12 @@ struct AppState { std::unordered_map table_col_filters; char table_filter_input[96] = {}; // buffer del popup activo int table_filter_pending_col = -1; // col_user_id en edicion + // Drill-in por grupo (issue 0035d): cuando esta seteado, la tabla solo + // muestra filas cuyo `group_id` coincida. Header muestra "Group: + // (N)" + boton "Clear group filter". RAM-only — al cerrar el panel se + // limpia. + std::string table_filter_group_id; // "" = sin filtro + std::string table_filter_group_name; // label legible // ---- Type Editor (issue 0007) ------------------------------------------ // Draft del editor de tipos. Se inicializa con una copia de parsed_types