Merge issue/0035d-tableview-drill-in

This commit is contained in:
2026-05-03 14:57:26 +02:00
5 changed files with 99 additions and 8 deletions
+27 -1
View File
@@ -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);
+1
View File
@@ -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
+26 -5
View File
@@ -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;
+38 -2
View File
@@ -1733,12 +1733,41 @@ void render_one_table(AppState& app, std::vector<int>& 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<std::string> types_present;
types_present.reserve(8);
{
std::unordered_set<std::string> 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)
+7
View File
@@ -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<int, std::string> 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: <name>
// (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