merge: issue/0004-table-view — vista tabla
This commit is contained in:
@@ -772,6 +772,44 @@ bool entity_list_by_tags(const char* db_path,
|
||||
return true;
|
||||
}
|
||||
|
||||
bool entity_list_rows(const char* db_path,
|
||||
std::vector<EntityRowSnapshot>* out)
|
||||
{
|
||||
if (!db_path || !out) return false;
|
||||
out->clear();
|
||||
sqlite3* db = nullptr;
|
||||
if (sqlite3_open_v2(db_path, &db, SQLITE_OPEN_READONLY, nullptr) != SQLITE_OK) {
|
||||
if (db) sqlite3_close(db);
|
||||
return false;
|
||||
}
|
||||
const char* sql =
|
||||
"SELECT id, COALESCE(name,''), COALESCE(type_ref,''), "
|
||||
" COALESCE(status,''), COALESCE(updated_at,'') "
|
||||
"FROM entities ORDER BY type_ref, name";
|
||||
sqlite3_stmt* st = nullptr;
|
||||
if (sqlite3_prepare_v2(db, sql, -1, &st, nullptr) != SQLITE_OK) {
|
||||
sqlite3_close(db);
|
||||
return false;
|
||||
}
|
||||
while (sqlite3_step(st) == SQLITE_ROW) {
|
||||
EntityRowSnapshot r;
|
||||
const unsigned char* a0 = sqlite3_column_text(st, 0);
|
||||
const unsigned char* a1 = sqlite3_column_text(st, 1);
|
||||
const unsigned char* a2 = sqlite3_column_text(st, 2);
|
||||
const unsigned char* a3 = sqlite3_column_text(st, 3);
|
||||
const unsigned char* a4 = sqlite3_column_text(st, 4);
|
||||
r.id = a0 ? (const char*)a0 : "";
|
||||
r.name = a1 ? (const char*)a1 : "";
|
||||
r.type_ref = a2 ? (const char*)a2 : "";
|
||||
r.status = a3 ? (const char*)a3 : "";
|
||||
r.updated_at = a4 ? (const char*)a4 : "";
|
||||
out->push_back(std::move(r));
|
||||
}
|
||||
sqlite3_finalize(st);
|
||||
sqlite3_close(db);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Index user_data -> sql id
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
@@ -122,6 +122,21 @@ bool entity_list_by_tags(const char* db_path,
|
||||
const std::vector<std::string>& tags,
|
||||
std::vector<std::string>* out_ids);
|
||||
|
||||
// Snapshot ligero por entidad para la vista tabla (issue 0004). No incluye
|
||||
// metadata ni notes — solo identidad + estado para tabular y ordenar.
|
||||
struct EntityRowSnapshot {
|
||||
std::string id;
|
||||
std::string name;
|
||||
std::string type_ref;
|
||||
std::string status;
|
||||
std::string updated_at;
|
||||
};
|
||||
|
||||
// Carga todas las filas de `entities` ordenadas por type_ref, name. Tolera BD
|
||||
// sin la columna `status` o `updated_at` — esos campos quedan vacios.
|
||||
bool entity_list_rows(const char* db_path,
|
||||
std::vector<EntityRowSnapshot>* out);
|
||||
|
||||
// Mapa user_data (FNV1a hash) -> sql id. Se reconstruye despues de cada
|
||||
// carga del grafo (graph_sources usa FNV1a sobre id como user_data).
|
||||
struct EntityIndex {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
---
|
||||
id: 0004
|
||||
title: Vista tabla — entidades agrupadas por tipo
|
||||
status: pending
|
||||
status: completed
|
||||
priority: medium
|
||||
created: 2026-04-30
|
||||
completed: 2026-05-01
|
||||
---
|
||||
|
||||
## Objetivo
|
||||
@@ -253,6 +253,26 @@ static bool load_input() {
|
||||
ge::views_reset_visibility(g_app);
|
||||
ge::views_apply_visibility(g_app);
|
||||
|
||||
// Cache de la vista tabla (issue 0004) — pull bulk + neighbors desde grafo.
|
||||
{
|
||||
std::vector<ge::EntityRowSnapshot> snap;
|
||||
if (g_input.uri && ge::entity_list_rows(g_input.uri, &snap)) {
|
||||
g_app.table_rows.clear();
|
||||
g_app.table_rows.reserve(snap.size());
|
||||
for (auto& s : snap) {
|
||||
ge::AppState::TableRow tr;
|
||||
tr.id = std::move(s.id);
|
||||
tr.name = std::move(s.name);
|
||||
tr.type_ref = std::move(s.type_ref);
|
||||
tr.status = std::move(s.status);
|
||||
tr.updated_at = std::move(s.updated_at);
|
||||
g_app.table_rows.push_back(std::move(tr));
|
||||
}
|
||||
ge::views_table_refresh_indices(g_app);
|
||||
g_app.table_cache_dirty = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Inspector: refresca caches (tags distintas, lista de tipos) y limpia
|
||||
// cualquier draft anterior. El draft se cargara cuando el usuario
|
||||
// seleccione un nodo en el render loop.
|
||||
@@ -451,6 +471,7 @@ static fn_ui::PanelToggle g_panels[] = {
|
||||
{"Stats", nullptr, &g_app.panel_stats},
|
||||
{"Note", nullptr, &g_app.panel_note},
|
||||
{"Types", nullptr, &g_app.panel_type_editor},
|
||||
{"Table", nullptr, &g_app.panel_table},
|
||||
};
|
||||
|
||||
static void render() {
|
||||
@@ -668,6 +689,23 @@ static void render() {
|
||||
ge::views_reset_visibility(g_app);
|
||||
ge::views_apply_visibility(g_app);
|
||||
|
||||
// Refresh table cache (issue 0004).
|
||||
std::vector<ge::EntityRowSnapshot> snap;
|
||||
if (ge::entity_list_rows(g_input.uri, &snap)) {
|
||||
g_app.table_rows.clear();
|
||||
g_app.table_rows.reserve(snap.size());
|
||||
for (auto& s : snap) {
|
||||
ge::AppState::TableRow tr;
|
||||
tr.id = std::move(s.id);
|
||||
tr.name = std::move(s.name);
|
||||
tr.type_ref = std::move(s.type_ref);
|
||||
tr.status = std::move(s.status);
|
||||
tr.updated_at = std::move(s.updated_at);
|
||||
g_app.table_rows.push_back(std::move(tr));
|
||||
}
|
||||
ge::views_table_refresh_indices(g_app);
|
||||
}
|
||||
|
||||
// Restablece posiciones guardadas. Los nodos nuevos no tienen
|
||||
// posicion en el layout_store y caen en (0,0).
|
||||
int restored = ge::layout_store_load(g_graph_hash, g_graph);
|
||||
@@ -939,6 +977,12 @@ static void render() {
|
||||
ge::views_type_editor(g_app);
|
||||
ge::views_type_editor_delete_modal(g_app);
|
||||
|
||||
// Table view (issue 0004) — flotante, dockeable.
|
||||
ImGui::SetNextWindowPos (ImVec2(vp->WorkPos.x + W * 0.15f, top + 60.0f),
|
||||
ImGuiCond_FirstUseEver);
|
||||
ImGui::SetNextWindowSize(ImVec2(820.0f, 520.0f), ImGuiCond_FirstUseEver);
|
||||
ge::views_table(g_app);
|
||||
|
||||
g_first_render = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -1391,6 +1391,214 @@ bool views_open_modal(AppState& app) {
|
||||
return opened;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Table view (issue 0004)
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
void views_table_refresh_indices(AppState& app) {
|
||||
if (!app.graph) return;
|
||||
GraphData& g = *app.graph;
|
||||
|
||||
// Degree map: user_data -> count.
|
||||
std::unordered_map<uint64_t, int> deg;
|
||||
deg.reserve((size_t)g.node_count * 2);
|
||||
for (int i = 0; i < g.edge_count; ++i) {
|
||||
const GraphEdge& e = g.edges[i];
|
||||
if (e.source < (uint32_t)g.node_count) deg[g.nodes[e.source].user_data]++;
|
||||
if (e.target < (uint32_t)g.node_count) deg[g.nodes[e.target].user_data]++;
|
||||
}
|
||||
|
||||
for (auto& r : app.table_rows) {
|
||||
uint64_t h = fnv1a64_id(r.id.c_str());
|
||||
r.node_idx = g.find_node_by_user_data(h);
|
||||
auto it = deg.find(h);
|
||||
r.neighbors = (it == deg.end()) ? 0 : it->second;
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// Comparador estable para ImGuiTableSortSpecs.
|
||||
struct TableSortCtx {
|
||||
const ImGuiTableSortSpecs* specs;
|
||||
};
|
||||
TableSortCtx g_table_sort_ctx;
|
||||
|
||||
bool table_row_lt(const AppState::TableRow& a, const AppState::TableRow& b) {
|
||||
const ImGuiTableSortSpecs* specs = g_table_sort_ctx.specs;
|
||||
if (!specs) return a.name < b.name;
|
||||
for (int n = 0; n < specs->SpecsCount; ++n) {
|
||||
const ImGuiTableColumnSortSpecs& s = specs->Specs[n];
|
||||
int delta = 0;
|
||||
switch (s.ColumnUserID) {
|
||||
case 0: delta = a.id.compare(b.id); break;
|
||||
case 1: delta = a.name.compare(b.name); break;
|
||||
case 2: delta = a.type_ref.compare(b.type_ref); break;
|
||||
case 3: delta = a.status.compare(b.status); break;
|
||||
case 4: delta = a.updated_at.compare(b.updated_at); break;
|
||||
case 5: delta = (a.neighbors < b.neighbors) ? -1
|
||||
: (a.neighbors > b.neighbors) ? 1 : 0; break;
|
||||
default: break;
|
||||
}
|
||||
if (delta != 0) {
|
||||
return (s.SortDirection == ImGuiSortDirection_Ascending) ? (delta < 0) : (delta > 0);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ci_contains(const std::string& hay, const char* needle) {
|
||||
if (!needle || !*needle) return true;
|
||||
auto lower = [](char c){ return (char)std::tolower((unsigned char)c); };
|
||||
std::string h; h.reserve(hay.size());
|
||||
for (char c : hay) h.push_back(lower(c));
|
||||
std::string n;
|
||||
for (const char* p = needle; *p; ++p) n.push_back(lower(*p));
|
||||
return h.find(n) != std::string::npos;
|
||||
}
|
||||
|
||||
void render_one_table(AppState& app, std::vector<int>& visible_indices) {
|
||||
ImGuiTableFlags flags =
|
||||
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
|
||||
ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable |
|
||||
ImGuiTableFlags_Sortable | ImGuiTableFlags_ScrollY |
|
||||
ImGuiTableFlags_SizingStretchProp;
|
||||
if (!ImGui::BeginTable("##tablev", 6, flags)) return;
|
||||
|
||||
ImGui::TableSetupScrollFreeze(0, 1);
|
||||
ImGui::TableSetupColumn("id", ImGuiTableColumnFlags_DefaultSort, 0, 0);
|
||||
ImGui::TableSetupColumn("name", ImGuiTableColumnFlags_None, 0, 1);
|
||||
ImGui::TableSetupColumn("type", ImGuiTableColumnFlags_None, 0, 2);
|
||||
ImGui::TableSetupColumn("status", ImGuiTableColumnFlags_None, 0, 3);
|
||||
ImGui::TableSetupColumn("updated_at", ImGuiTableColumnFlags_None, 0, 4);
|
||||
ImGui::TableSetupColumn("neighbors", ImGuiTableColumnFlags_WidthFixed,
|
||||
80.0f, 5);
|
||||
ImGui::TableHeadersRow();
|
||||
|
||||
ImGuiTableSortSpecs* specs = ImGui::TableGetSortSpecs();
|
||||
if (specs && specs->SpecsDirty) {
|
||||
g_table_sort_ctx.specs = specs;
|
||||
std::sort(visible_indices.begin(), visible_indices.end(),
|
||||
[&app](int a, int b) {
|
||||
return table_row_lt(app.table_rows[a], app.table_rows[b]);
|
||||
});
|
||||
specs->SpecsDirty = false;
|
||||
}
|
||||
|
||||
ImGuiListClipper clipper;
|
||||
clipper.Begin((int)visible_indices.size());
|
||||
while (clipper.Step()) {
|
||||
for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row) {
|
||||
int ri = visible_indices[row];
|
||||
const auto& r = app.table_rows[ri];
|
||||
ImGui::TableNextRow();
|
||||
ImGui::PushID(ri);
|
||||
|
||||
ImGui::TableSetColumnIndex(0);
|
||||
char sel_lbl[256];
|
||||
std::snprintf(sel_lbl, sizeof(sel_lbl), "%s##sel", r.id.c_str());
|
||||
bool is_sel = (app.viewport && r.node_idx >= 0
|
||||
&& graph_viewport_is_selected(*app.viewport, r.node_idx));
|
||||
if (ImGui::Selectable(sel_lbl, is_sel,
|
||||
ImGuiSelectableFlags_SpanAllColumns)) {
|
||||
if (r.node_idx >= 0 && app.graph && app.viewport) {
|
||||
graph_viewport_clear_selection(*app.graph, *app.viewport);
|
||||
graph_viewport_add_to_selection(*app.graph, *app.viewport,
|
||||
r.node_idx);
|
||||
app.filter_focus_target = r.node_idx;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::TableSetColumnIndex(1);
|
||||
ImGui::TextUnformatted(r.name.c_str());
|
||||
ImGui::TableSetColumnIndex(2);
|
||||
ImGui::TextUnformatted(r.type_ref.c_str());
|
||||
ImGui::TableSetColumnIndex(3);
|
||||
ImGui::TextUnformatted(r.status.c_str());
|
||||
ImGui::TableSetColumnIndex(4);
|
||||
ImGui::TextUnformatted(r.updated_at.c_str());
|
||||
ImGui::TableSetColumnIndex(5);
|
||||
ImGui::Text("%d", r.neighbors);
|
||||
|
||||
ImGui::PopID();
|
||||
}
|
||||
}
|
||||
ImGui::EndTable();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void views_table(AppState& app) {
|
||||
if (!app.panel_table) return;
|
||||
if (!ImGui::Begin("Table", &app.panel_table)) {
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
// Toolbar superior: search + show all.
|
||||
ImGui::SetNextItemWidth(220);
|
||||
ImGui::InputTextWithHint("##tsearch", TI_SEARCH " filter name/id...",
|
||||
app.table_search_buf, sizeof(app.table_search_buf));
|
||||
ImGui::SameLine();
|
||||
ImGui::Checkbox("Show all types", &app.table_show_all);
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("%zu rows", app.table_rows.size());
|
||||
|
||||
if (app.table_rows.empty()) {
|
||||
ImGui::TextDisabled("(no entities loaded)");
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
// Indices por tipo.
|
||||
std::vector<std::string> types_present;
|
||||
types_present.reserve(8);
|
||||
{
|
||||
std::unordered_set<std::string> seen;
|
||||
for (const auto& r : app.table_rows) {
|
||||
if (seen.insert(r.type_ref).second) types_present.push_back(r.type_ref);
|
||||
}
|
||||
std::sort(types_present.begin(), types_present.end());
|
||||
}
|
||||
|
||||
auto build_visible = [&](const char* type_filter) {
|
||||
std::vector<int> v;
|
||||
v.reserve(app.table_rows.size());
|
||||
for (size_t i = 0; i < app.table_rows.size(); ++i) {
|
||||
const auto& r = app.table_rows[i];
|
||||
if (type_filter && r.type_ref != type_filter) continue;
|
||||
if (app.table_search_buf[0]
|
||||
&& !ci_contains(r.name, app.table_search_buf)
|
||||
&& !ci_contains(r.id, app.table_search_buf)) continue;
|
||||
v.push_back((int)i);
|
||||
}
|
||||
return v;
|
||||
};
|
||||
|
||||
if (app.table_show_all) {
|
||||
auto visible = build_visible(nullptr);
|
||||
ImGui::TextDisabled("All types — %zu visible", visible.size());
|
||||
render_one_table(app, visible);
|
||||
} else if (ImGui::BeginTabBar("##ttabs")) {
|
||||
for (size_t i = 0; i < types_present.size(); ++i) {
|
||||
const std::string& t = types_present[i];
|
||||
char lbl[96];
|
||||
std::snprintf(lbl, sizeof(lbl), "%s##tt%zu",
|
||||
t.empty() ? "(none)" : t.c_str(), i);
|
||||
if (ImGui::BeginTabItem(lbl)) {
|
||||
app.table_active_tab = (int)i;
|
||||
auto visible = build_visible(t.c_str());
|
||||
ImGui::TextDisabled("%zu rows visible", visible.size());
|
||||
render_one_table(app, visible);
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
}
|
||||
ImGui::EndTabBar();
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Type Editor (issue 0007)
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
@@ -137,6 +137,26 @@ struct AppState {
|
||||
std::vector<std::string> insp_tag_suggestions;
|
||||
std::vector<std::string> insp_type_options;
|
||||
|
||||
// ---- Table view (issue 0004) -------------------------------------------
|
||||
// Vista tabular dockeable. Tabs por type_ref del grafo activo + opcional
|
||||
// "All". Click selecciona el nodo en el viewport (mismo flujo que el
|
||||
// Selectable del Inspector).
|
||||
struct TableRow {
|
||||
std::string id;
|
||||
std::string name;
|
||||
std::string type_ref;
|
||||
std::string status;
|
||||
std::string updated_at;
|
||||
int neighbors = 0;
|
||||
int node_idx = -1;
|
||||
};
|
||||
bool panel_table = false;
|
||||
std::vector<TableRow> table_rows; // snapshot, refrescado tras load/reload
|
||||
bool table_cache_dirty = true;
|
||||
char table_search_buf[96] = {};
|
||||
bool table_show_all = false;
|
||||
int table_active_tab = 0;
|
||||
|
||||
// ---- Type Editor (issue 0007) ------------------------------------------
|
||||
// Draft del editor de tipos. Se inicializa con una copia de parsed_types
|
||||
// tras cargar el grafo. Save reescribe `types.yaml` y dispara
|
||||
@@ -223,6 +243,18 @@ EntityRecord views_inspector_build_record(const AppState& app);
|
||||
// al cambiar de proyecto.
|
||||
void views_inspector_clear_draft(AppState& app);
|
||||
|
||||
// ---- Table view (issue 0004) --------------------------------------------
|
||||
|
||||
// Renderiza el panel "Table". Lee de app.table_rows; el caller ya ha hecho el
|
||||
// build/refresh tras cargar el grafo. Click en fila selecciona el nodo en el
|
||||
// viewport (mismo flujo que el Selectable del Inspector). Filtro por
|
||||
// substring sobre name/id en la cabecera.
|
||||
void views_table(AppState& app);
|
||||
|
||||
// Recompute neighbors[] y node_idx[] de las filas existentes a partir del
|
||||
// grafo cargado. Llamar tras cargar el grafo o tras una mutacion.
|
||||
void views_table_refresh_indices(AppState& app);
|
||||
|
||||
// ---- Type Editor (issue 0007) -------------------------------------------
|
||||
|
||||
// Renderiza el panel "Types" — tabs Entities/Relations, lista a la izquierda
|
||||
|
||||
Reference in New Issue
Block a user