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:
2026-05-03 14:57:20 +02:00
parent eff273a2d4
commit b67da92e18
5 changed files with 99 additions and 8 deletions
+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)