feat(0035d): doble click en Group abre tableview filtrado por group_id
- entity_ops: EntityRowSnapshot.group_id + SQL con COALESCE(group_id,'') + deteccion via PRAGMA para BDs viejas sin la columna. - views.h: TableRow.group_id + AppState.table_filter_group_id / table_filter_group_name (RAM-only). - main.cpp: dispatch en want_open_note — si type_ref == "Group", setea filtro de grupo + abre panel Table en vez de Note. Reset de search buf y col_filters al entrar al drill-in para que el usuario vea todo el contenido del grupo. - views.cpp: build_visible compone group_id con search/tabs/col_filters (AND). types_present se reduce a tipos presentes en el grupo cuando hay drill-in activo. Header pintado en amarillo con TI_FOLDER + contador + boton "Clear group filter". Al cerrarse el panel se limpia el filtro automaticamente. Tests: pytest 35 passed (WSL) / 24 passed + 11 skipped (Windows). Refs: issues/0035d-tableview-drill-in.md
This commit is contained in:
+27
-1
@@ -782,10 +782,32 @@ bool entity_list_rows(const char* db_path,
|
|||||||
if (db) sqlite3_close(db);
|
if (db) sqlite3_close(db);
|
||||||
return false;
|
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,''), "
|
"SELECT id, COALESCE(name,''), COALESCE(type_ref,''), "
|
||||||
" COALESCE(status,''), COALESCE(updated_at,'') "
|
" COALESCE(status,''), COALESCE(updated_at,'') "
|
||||||
"FROM entities ORDER BY type_ref, name";
|
"FROM entities ORDER BY type_ref, name";
|
||||||
|
const char* sql = has_group_id ? sql_with : sql_without;
|
||||||
sqlite3_stmt* st = nullptr;
|
sqlite3_stmt* st = nullptr;
|
||||||
if (sqlite3_prepare_v2(db, sql, -1, &st, nullptr) != SQLITE_OK) {
|
if (sqlite3_prepare_v2(db, sql, -1, &st, nullptr) != SQLITE_OK) {
|
||||||
sqlite3_close(db);
|
sqlite3_close(db);
|
||||||
@@ -803,6 +825,10 @@ bool entity_list_rows(const char* db_path,
|
|||||||
r.type_ref = a2 ? (const char*)a2 : "";
|
r.type_ref = a2 ? (const char*)a2 : "";
|
||||||
r.status = a3 ? (const char*)a3 : "";
|
r.status = a3 ? (const char*)a3 : "";
|
||||||
r.updated_at = a4 ? (const char*)a4 : "";
|
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));
|
out->push_back(std::move(r));
|
||||||
}
|
}
|
||||||
sqlite3_finalize(st);
|
sqlite3_finalize(st);
|
||||||
|
|||||||
@@ -130,6 +130,7 @@ struct EntityRowSnapshot {
|
|||||||
std::string type_ref;
|
std::string type_ref;
|
||||||
std::string status;
|
std::string status;
|
||||||
std::string updated_at;
|
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
|
// Carga todas las filas de `entities` ordenadas por type_ref, name. Tolera BD
|
||||||
|
|||||||
@@ -729,6 +729,7 @@ static bool load_input(bool first_load) {
|
|||||||
tr.type_ref = std::move(s.type_ref);
|
tr.type_ref = std::move(s.type_ref);
|
||||||
tr.status = std::move(s.status);
|
tr.status = std::move(s.status);
|
||||||
tr.updated_at = std::move(s.updated_at);
|
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));
|
g_app.table_rows.push_back(std::move(tr));
|
||||||
}
|
}
|
||||||
ge::views_table_refresh_indices(g_app);
|
ge::views_table_refresh_indices(g_app);
|
||||||
@@ -1558,6 +1559,7 @@ static void render() {
|
|||||||
tr.type_ref = std::move(s.type_ref);
|
tr.type_ref = std::move(s.type_ref);
|
||||||
tr.status = std::move(s.status);
|
tr.status = std::move(s.status);
|
||||||
tr.updated_at = std::move(s.updated_at);
|
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));
|
g_app.table_rows.push_back(std::move(tr));
|
||||||
}
|
}
|
||||||
ge::views_table_refresh_indices(g_app);
|
ge::views_table_refresh_indices(g_app);
|
||||||
@@ -1855,21 +1857,40 @@ static void render() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Note editor — abrir / guardar.
|
// 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
|
if (g_app.want_open_note && g_app.open_note_target >= 0
|
||||||
&& g_app.open_note_target < g_graph.node_count) {
|
&& g_app.open_note_target < g_graph.node_count) {
|
||||||
int n = g_app.open_note_target;
|
int n = g_app.open_note_target;
|
||||||
const char* sql_id = ge::entity_index_lookup(g_idx, g_graph.nodes[n].user_data);
|
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;
|
std::string md;
|
||||||
ge::entity_get_notes(g_app.input_db_path.c_str(), sql_id, &md);
|
ge::entity_get_notes(g_app.input_db_path.c_str(), sql_id, &md);
|
||||||
g_app.note_node = n;
|
g_app.note_node = n;
|
||||||
g_app.note_entity_id = sql_id;
|
g_app.note_entity_id = sql_id;
|
||||||
const char* lbl = graph::graph_label(&g_graph, g_graph.nodes[n].label_idx);
|
const char* lbl = graph::graph_label(&g_graph, g_graph.nodes[n].label_idx);
|
||||||
g_app.note_entity_label = lbl ? lbl : "";
|
g_app.note_entity_label = lbl ? lbl : "";
|
||||||
uint16_t tid = g_graph.nodes[n].type_id;
|
g_app.note_entity_type = type_name;
|
||||||
g_app.note_entity_type = (tid < (uint16_t)g_graph.type_count
|
|
||||||
&& g_graph.types[tid].name)
|
|
||||||
? g_graph.types[tid].name : "";
|
|
||||||
// Asegura buffer >= max(64KB, contenido + holgura).
|
// Asegura buffer >= max(64KB, contenido + holgura).
|
||||||
size_t need = md.size() + 4096;
|
size_t need = md.size() + 4096;
|
||||||
if (need < 65536) need = 65536;
|
if (need < 65536) need = 65536;
|
||||||
|
|||||||
@@ -1733,12 +1733,41 @@ void render_one_table(AppState& app, std::vector<int>& visible_indices) {
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void views_table(AppState& app) {
|
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)) {
|
if (!ImGui::Begin("Table", &app.panel_table)) {
|
||||||
ImGui::End();
|
ImGui::End();
|
||||||
return;
|
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.
|
// Toolbar superior: search + show all.
|
||||||
ImGui::SetNextItemWidth(220);
|
ImGui::SetNextItemWidth(220);
|
||||||
ImGui::InputTextWithHint("##tsearch", TI_SEARCH " filter name/id...",
|
ImGui::InputTextWithHint("##tsearch", TI_SEARCH " filter name/id...",
|
||||||
@@ -1777,12 +1806,15 @@ void views_table(AppState& app) {
|
|||||||
return;
|
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;
|
std::vector<std::string> types_present;
|
||||||
types_present.reserve(8);
|
types_present.reserve(8);
|
||||||
{
|
{
|
||||||
std::unordered_set<std::string> seen;
|
std::unordered_set<std::string> seen;
|
||||||
for (const auto& r : app.table_rows) {
|
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);
|
if (seen.insert(r.type_ref).second) types_present.push_back(r.type_ref);
|
||||||
}
|
}
|
||||||
std::sort(types_present.begin(), types_present.end());
|
std::sort(types_present.begin(), types_present.end());
|
||||||
@@ -1793,6 +1825,10 @@ void views_table(AppState& app) {
|
|||||||
v.reserve(app.table_rows.size());
|
v.reserve(app.table_rows.size());
|
||||||
for (size_t i = 0; i < app.table_rows.size(); ++i) {
|
for (size_t i = 0; i < app.table_rows.size(); ++i) {
|
||||||
const auto& r = app.table_rows[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 (type_filter && r.type_ref != type_filter) continue;
|
||||||
if (app.table_search_buf[0]
|
if (app.table_search_buf[0]
|
||||||
&& !ci_contains(r.name, app.table_search_buf)
|
&& !ci_contains(r.name, app.table_search_buf)
|
||||||
|
|||||||
@@ -210,6 +210,7 @@ struct AppState {
|
|||||||
std::string type_ref;
|
std::string type_ref;
|
||||||
std::string status;
|
std::string status;
|
||||||
std::string updated_at;
|
std::string updated_at;
|
||||||
|
std::string group_id; // si pertenece a un Group, su sql id; "" si no
|
||||||
int neighbors = 0;
|
int neighbors = 0;
|
||||||
int node_idx = -1;
|
int node_idx = -1;
|
||||||
};
|
};
|
||||||
@@ -225,6 +226,12 @@ struct AppState {
|
|||||||
std::unordered_map<int, std::string> table_col_filters;
|
std::unordered_map<int, std::string> table_col_filters;
|
||||||
char table_filter_input[96] = {}; // buffer del popup activo
|
char table_filter_input[96] = {}; // buffer del popup activo
|
||||||
int table_filter_pending_col = -1; // col_user_id en edicion
|
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) ------------------------------------------
|
// ---- Type Editor (issue 0007) ------------------------------------------
|
||||||
// Draft del editor de tipos. Se inicializa con una copia de parsed_types
|
// Draft del editor de tipos. Se inicializa con una copia de parsed_types
|
||||||
|
|||||||
Reference in New Issue
Block a user