feat(types): Type Editor panel — CRUD de tipos en vivo (issue 0007)

- views_type_editor: panel "Types" con tabs Entities/Relations.
  Entities: name, color picker, shape combo, icon (ti-* + cp preview),
  principal_field combo, tabla de Fields (string/int/float/bool/date/url/enum)
  con required y enum values CSV; up/down/X por fila.
  Relations: name, color, style.
  Footer Save / Reload from disk + indicador dirty + error inline.
- views_type_editor_delete_modal: confirm con conteo de entidades en uso.
- types_registry: shape_name() + shape: emit en types_save_yaml para
  round-trip estable de la cosmetica editada en UI.
- main.cpp: panel "Types" en g_panels; init types_draft tras load_input;
  want_types_save -> save + apply_types_yaml + rebuild atlas + bind +
  refresh inspector caches; want_types_reload simetrico; conteo de
  uso desde operations.db cuando se abre el modal de delete.
This commit is contained in:
2026-05-01 00:42:30 +02:00
parent 69f1afcf9e
commit f80348d604
5 changed files with 549 additions and 1 deletions
@@ -1,9 +1,10 @@
---
id: 0007
title: Type Editor panel — CRUD de tipos desde la app
status: pending
status: completed
priority: medium
created: 2026-04-30
completed: 2026-05-01
depends_on: [0005, 0006]
---
+94
View File
@@ -26,6 +26,8 @@
#include "entity_ops.h"
#include "project_manager.h"
#include "../../../../cpp/vendor/sqlite3/sqlite3.h"
#include <cstdio>
#include <cstdlib>
#include <cstring>
@@ -206,6 +208,10 @@ static bool load_input() {
g_app.parsed_types = std::move(pt);
}
}
// Inicializar el draft del Type Editor con copia de parsed_types (0007).
g_app.types_draft = g_app.parsed_types;
g_app.types_dirty = false;
g_app.types_save_error.clear();
// Restablecer viewport state (preserva camara user-visible)
g_viewport.selection.clear();
@@ -444,6 +450,7 @@ static fn_ui::PanelToggle g_panels[] = {
{"Inspector", nullptr, &g_app.panel_inspector},
{"Stats", nullptr, &g_app.panel_stats},
{"Note", nullptr, &g_app.panel_note},
{"Types", nullptr, &g_app.panel_type_editor},
};
static void render() {
@@ -573,6 +580,86 @@ static void render() {
load_input();
}
// ---- Type Editor (issue 0007) ----
if (g_app.want_types_save) {
g_app.want_types_save = false;
g_app.types_save_error.clear();
if (g_types_path.empty()) {
g_app.types_save_error =
"No hay types.yaml asignado (abre un proyecto o usa --types).";
} else {
std::string err;
if (!ge::types_save_yaml(g_types_path.c_str(),
g_app.types_draft, &err)) {
g_app.types_save_error = "Save failed: " + err;
} else {
std::fprintf(stdout,
"[graph_explorer] types.yaml saved -> %s\n",
g_types_path.c_str());
g_app.parsed_types = g_app.types_draft;
std::vector<uint16_t> cps =
ge::apply_types_yaml(g_graph, g_app.parsed_types);
if (g_atlas) { graph_icons_destroy(g_atlas); g_atlas = nullptr; }
g_atlas = ge::build_icon_atlas(cps);
g_atlas_bound = false;
g_gpu_dirty = true;
g_app.types_dirty = false;
ge::views_inspector_refresh_caches(g_app);
}
}
}
if (g_app.want_types_reload) {
g_app.want_types_reload = false;
g_app.types_save_error.clear();
if (g_types_path.empty()) {
// Sin types.yaml en disco: descarta el draft a parsed_types actual.
g_app.types_draft = g_app.parsed_types;
g_app.types_dirty = false;
} else {
ge::ParsedTypes pt;
std::string err;
if (!ge::types_load_yaml(g_types_path.c_str(), &pt, &err)) {
g_app.types_save_error = "Reload failed: " + err;
} else {
std::vector<uint16_t> cps = ge::apply_types_yaml(g_graph, pt);
if (g_atlas) { graph_icons_destroy(g_atlas); g_atlas = nullptr; }
g_atlas = ge::build_icon_atlas(cps);
g_atlas_bound = false;
g_gpu_dirty = true;
g_app.parsed_types = pt;
g_app.types_draft = std::move(pt);
g_app.types_dirty = false;
ge::views_inspector_refresh_caches(g_app);
}
}
}
// Conteo de uso para el modal de borrado (entidades activas en BD).
if (g_app.show_te_delete_modal && g_app.te_delete_use_count == 0
&& !g_app.input_db_path.empty()) {
const char* tname = nullptr;
if (g_app.te_pending_delete_e >= 0
&& g_app.te_pending_delete_e < (int)g_app.types_draft.entities.size()) {
tname = g_app.types_draft.entities[g_app.te_pending_delete_e].name.c_str();
}
if (tname && *tname) {
sqlite3* db = nullptr;
if (sqlite3_open_v2(g_app.input_db_path.c_str(), &db,
SQLITE_OPEN_READONLY, nullptr) == SQLITE_OK) {
sqlite3_stmt* st = nullptr;
if (sqlite3_prepare_v2(db,
"SELECT COUNT(*) FROM entities WHERE type_ref = ?",
-1, &st, nullptr) == SQLITE_OK) {
sqlite3_bind_text(st, 1, tname, -1, SQLITE_TRANSIENT);
if (sqlite3_step(st) == SQLITE_ROW) {
g_app.te_delete_use_count = sqlite3_column_int(st, 0);
}
sqlite3_finalize(st);
}
sqlite3_close(db);
}
}
}
// ---- Mutaciones (add/delete/duplicate/change_type) ----
auto reload_after_mutation = [&]() {
graph::GraphLoadStats stats{};
@@ -845,6 +932,13 @@ static void render() {
ImGui::SetNextWindowSize(ImVec2(700.0f, 480.0f), ImGuiCond_FirstUseEver);
ge::views_note(g_app);
// Type Editor (issue 0007) — flotante, dockeable.
ImGui::SetNextWindowPos (ImVec2(vp->WorkPos.x + W * 0.20f, top + 40.0f),
ImGuiCond_FirstUseEver);
ImGui::SetNextWindowSize(ImVec2(720.0f, 500.0f), ImGuiCond_FirstUseEver);
ge::views_type_editor(g_app);
ge::views_type_editor_delete_modal(g_app);
g_first_render = false;
}
+14
View File
@@ -410,6 +410,18 @@ const char* style_name(uint8_t s) {
}
}
const char* shape_name(uint8_t s) {
switch (s) {
case SHAPE_CIRCLE: return "circle";
case SHAPE_SQUARE: return "square";
case SHAPE_DIAMOND: return "diamond";
case SHAPE_HEX: return "hex";
case SHAPE_TRIANGLE: return "triangle";
case SHAPE_ROUNDED_SQUARE: return "rounded_square";
default: return "";
}
}
// Serializa un FieldSpec como inline-map: "{ name: X, type: Y, required: true, values: [a,b,c] }".
std::string serialize_field(const FieldSpec& f) {
std::string out = "{ name: ";
@@ -454,6 +466,8 @@ bool types_save_yaml(const char* path, const ParsedTypes& types,
f << " - name: " << e.name << "\n";
std::string col = color_to_hex(e.color);
if (!col.empty()) f << " color: \"" << col << "\"\n";
const char* sh = shape_name(e.shape);
if (sh && *sh) f << " shape: " << sh << "\n";
if (!e.icon_name.empty()) f << " icon: " << e.icon_name << "\n";
if (!e.principal_field.empty())
f << " principal_field: " << e.principal_field << "\n";
+409
View File
@@ -1391,4 +1391,413 @@ bool views_open_modal(AppState& app) {
return opened;
}
// ----------------------------------------------------------------------------
// Type Editor (issue 0007)
// ----------------------------------------------------------------------------
namespace {
const char* k_shape_names[] = {
"(use type)", "circle", "square", "diamond", "hex", "triangle", "rounded_square",
};
constexpr int k_shape_count = (int)(sizeof(k_shape_names) / sizeof(k_shape_names[0]));
const char* k_style_names[] = {
"(use type)", "solid", "dashed", "dotted",
};
constexpr int k_style_count = (int)(sizeof(k_style_names) / sizeof(k_style_names[0]));
const char* k_field_kind_names[] = {
"string", "int", "float", "bool", "date", "url", "enum",
};
constexpr int k_field_kind_count = (int)(sizeof(k_field_kind_names) / sizeof(k_field_kind_names[0]));
ImVec4 abgr_to_imvec4_full(uint32_t c) {
return ImVec4(
(float)( c & 0xFF) / 255.0f,
(float)((c >> 8) & 0xFF) / 255.0f,
(float)((c >> 16) & 0xFF) / 255.0f,
(float)((c >> 24) & 0xFF) / 255.0f);
}
uint32_t imvec4_to_abgr(const ImVec4& v) {
auto clamp01 = [](float x) { return x < 0 ? 0.f : (x > 1 ? 1.f : x); };
uint8_t r = (uint8_t)(clamp01(v.x) * 255.0f + 0.5f);
uint8_t g = (uint8_t)(clamp01(v.y) * 255.0f + 0.5f);
uint8_t b = (uint8_t)(clamp01(v.z) * 255.0f + 0.5f);
uint8_t a = (uint8_t)(clamp01(v.w) * 255.0f + 0.5f);
return (uint32_t)r | ((uint32_t)g << 8) | ((uint32_t)b << 16) | ((uint32_t)a << 24);
}
} // namespace
void views_type_editor(AppState& app) {
if (!app.panel_type_editor) return;
if (!ImGui::Begin("Types", &app.panel_type_editor)) {
ImGui::End();
return;
}
if (ImGui::BeginTabBar("##te_tabs")) {
// ---- Entities tab --------------------------------------------------
if (ImGui::BeginTabItem("Entities")) {
app.te_tab_idx = 0;
ImGui::BeginChild("##te_left", ImVec2(160, 0), true);
for (int i = 0; i < (int)app.types_draft.entities.size(); ++i) {
ImGui::PushID(i);
bool sel = (i == app.te_entity_idx);
const auto& e = app.types_draft.entities[i];
if (ImGui::Selectable(e.name.empty() ? "(unnamed)" : e.name.c_str(), sel)) {
app.te_entity_idx = i;
}
ImGui::PopID();
}
ImGui::EndChild();
ImGui::SameLine();
ImGui::BeginGroup();
// +/- buttons
if (fn_ui::button(TI_PLUS " Add entity type", fn_ui::ButtonVariant::Subtle)) {
EntitySpec ne;
ne.name = "NewType";
app.types_draft.entities.push_back(std::move(ne));
app.te_entity_idx = (int)app.types_draft.entities.size() - 1;
app.types_dirty = true;
}
ImGui::SameLine();
bool can_del = (app.te_entity_idx >= 0
&& app.te_entity_idx < (int)app.types_draft.entities.size());
if (!can_del) ImGui::BeginDisabled();
if (fn_ui::button(TI_X " Delete", fn_ui::ButtonVariant::Subtle)) {
app.te_pending_delete_e = app.te_entity_idx;
app.te_pending_delete_r = -1;
app.show_te_delete_modal = true;
}
if (!can_del) ImGui::EndDisabled();
ImGui::Separator();
if (can_del) {
EntitySpec& e = app.types_draft.entities[app.te_entity_idx];
// Name
char namebuf[80];
std::snprintf(namebuf, sizeof(namebuf), "%s", e.name.c_str());
if (ImGui::InputText("Name", namebuf, sizeof(namebuf))) {
e.name = namebuf;
app.types_dirty = true;
}
// Color
ImVec4 col = abgr_to_imvec4_full(e.color ? e.color : 0xFF888888u);
if (ImGui::ColorEdit4("Color", (float*)&col,
ImGuiColorEditFlags_NoInputs
| ImGuiColorEditFlags_AlphaBar)) {
e.color = imvec4_to_abgr(col);
app.types_dirty = true;
}
// Shape
int sh_idx = (e.shape <= SHAPE_ROUNDED_SQUARE) ? (int)e.shape : 0;
if (ImGui::Combo("Shape", &sh_idx, k_shape_names, k_shape_count)) {
e.shape = (uint8_t)sh_idx;
app.types_dirty = true;
}
// Icon
char ibuf[64];
std::snprintf(ibuf, sizeof(ibuf), "%s", e.icon_name.c_str());
if (ImGui::InputText("Icon (ti-*)", ibuf, sizeof(ibuf))) {
e.icon_name = ibuf;
e.icon_cp = tabler_codepoint_by_name(ibuf);
app.types_dirty = true;
}
ImGui::SameLine();
ImGui::TextDisabled("cp=0x%04X", e.icon_cp);
// Principal field (combo entre fields existentes; permite "name" por default)
std::vector<const char*> pf_opts;
pf_opts.push_back("(name)");
for (const auto& fs : e.fields) pf_opts.push_back(fs.name.c_str());
int pf_idx = 0;
for (int i = 1; i < (int)pf_opts.size(); ++i) {
if (e.principal_field == pf_opts[i]) { pf_idx = i; break; }
}
if (e.principal_field.empty()) pf_idx = 0;
if (ImGui::Combo("Principal field", &pf_idx,
pf_opts.data(), (int)pf_opts.size())) {
e.principal_field = (pf_idx == 0) ? std::string()
: std::string(pf_opts[pf_idx]);
app.types_dirty = true;
}
// Fields table
ImGui::Spacing();
ImGui::TextUnformatted("Fields");
ImGui::Separator();
if (ImGui::BeginTable("##te_fields", 5,
ImGuiTableFlags_BordersInnerV
| ImGuiTableFlags_RowBg
| ImGuiTableFlags_SizingStretchProp)) {
ImGui::TableSetupColumn("name");
ImGui::TableSetupColumn("type", ImGuiTableColumnFlags_WidthFixed, 90.0f);
ImGui::TableSetupColumn("required", ImGuiTableColumnFlags_WidthFixed, 70.0f);
ImGui::TableSetupColumn("values", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, 60.0f);
ImGui::TableHeadersRow();
int del_idx = -1;
int up_idx = -1;
int dn_idx = -1;
for (int fi = 0; fi < (int)e.fields.size(); ++fi) {
FieldSpec& fs = e.fields[fi];
ImGui::PushID(fi);
ImGui::TableNextRow();
// name
ImGui::TableSetColumnIndex(0);
ImGui::SetNextItemWidth(-FLT_MIN);
char fbuf[64];
std::snprintf(fbuf, sizeof(fbuf), "%s", fs.name.c_str());
if (ImGui::InputText("##fname", fbuf, sizeof(fbuf))) {
fs.name = fbuf;
app.types_dirty = true;
}
// type
ImGui::TableSetColumnIndex(1);
ImGui::SetNextItemWidth(-FLT_MIN);
int kidx = (int)fs.kind;
if (kidx < 0 || kidx >= k_field_kind_count) kidx = 0;
if (ImGui::Combo("##ftype", &kidx,
k_field_kind_names, k_field_kind_count)) {
fs.kind = (FieldKind)kidx;
if (fs.kind != FK_ENUM) fs.enum_values.clear();
app.types_dirty = true;
}
// required
ImGui::TableSetColumnIndex(2);
if (ImGui::Checkbox("##freq", &fs.required)) app.types_dirty = true;
// values (CSV editable solo si enum)
ImGui::TableSetColumnIndex(3);
if (fs.kind == FK_ENUM) {
std::string csv;
for (size_t i = 0; i < fs.enum_values.size(); ++i) {
if (i) csv += ", ";
csv += fs.enum_values[i];
}
char vbuf[256];
std::snprintf(vbuf, sizeof(vbuf), "%s", csv.c_str());
ImGui::SetNextItemWidth(-FLT_MIN);
if (ImGui::InputText("##fval", vbuf, sizeof(vbuf))) {
fs.enum_values.clear();
std::string s = vbuf;
size_t start = 0;
while (start < s.size()) {
size_t end = s.find(',', start);
if (end == std::string::npos) end = s.size();
std::string tok = s.substr(start, end - start);
while (!tok.empty() && std::isspace((unsigned char)tok.front())) tok.erase(tok.begin());
while (!tok.empty() && std::isspace((unsigned char)tok.back())) tok.pop_back();
if (!tok.empty()) fs.enum_values.push_back(std::move(tok));
start = end + 1;
}
app.types_dirty = true;
}
} else {
ImGui::TextDisabled("-");
}
// controls
ImGui::TableSetColumnIndex(4);
if (ImGui::SmallButton(TI_X "##fd")) del_idx = fi;
ImGui::SameLine();
if (fi > 0 && ImGui::SmallButton("^##fu")) up_idx = fi;
ImGui::SameLine();
if (fi + 1 < (int)e.fields.size()
&& ImGui::SmallButton("v##fdn")) dn_idx = fi;
ImGui::PopID();
}
ImGui::EndTable();
if (del_idx >= 0) {
e.fields.erase(e.fields.begin() + del_idx);
app.types_dirty = true;
} else if (up_idx > 0) {
std::swap(e.fields[up_idx - 1], e.fields[up_idx]);
app.types_dirty = true;
} else if (dn_idx >= 0 && dn_idx + 1 < (int)e.fields.size()) {
std::swap(e.fields[dn_idx], e.fields[dn_idx + 1]);
app.types_dirty = true;
}
}
if (fn_ui::button(TI_PLUS " Add field", fn_ui::ButtonVariant::Subtle)) {
FieldSpec ns;
ns.name = "field" + std::to_string(e.fields.size() + 1);
e.fields.push_back(std::move(ns));
app.types_dirty = true;
}
} else {
ImGui::TextDisabled("(no entity selected)");
}
ImGui::EndGroup();
ImGui::EndTabItem();
}
// ---- Relations tab -------------------------------------------------
if (ImGui::BeginTabItem("Relations")) {
app.te_tab_idx = 1;
ImGui::BeginChild("##te_left_r", ImVec2(160, 0), true);
for (int i = 0; i < (int)app.types_draft.relations.size(); ++i) {
ImGui::PushID(i + 10000);
bool sel = (i == app.te_relation_idx);
const auto& r = app.types_draft.relations[i];
if (ImGui::Selectable(r.name.empty() ? "(unnamed)" : r.name.c_str(), sel)) {
app.te_relation_idx = i;
}
ImGui::PopID();
}
ImGui::EndChild();
ImGui::SameLine();
ImGui::BeginGroup();
if (fn_ui::button(TI_PLUS " Add relation type", fn_ui::ButtonVariant::Subtle)) {
RelationSpec nr;
nr.name = "new_relation";
app.types_draft.relations.push_back(std::move(nr));
app.te_relation_idx = (int)app.types_draft.relations.size() - 1;
app.types_dirty = true;
}
ImGui::SameLine();
bool can_del_r = (app.te_relation_idx >= 0
&& app.te_relation_idx < (int)app.types_draft.relations.size());
if (!can_del_r) ImGui::BeginDisabled();
if (fn_ui::button(TI_X " Delete", fn_ui::ButtonVariant::Subtle)) {
app.te_pending_delete_r = app.te_relation_idx;
app.te_pending_delete_e = -1;
app.show_te_delete_modal = true;
}
if (!can_del_r) ImGui::EndDisabled();
ImGui::Separator();
if (can_del_r) {
RelationSpec& r = app.types_draft.relations[app.te_relation_idx];
char rnbuf[80];
std::snprintf(rnbuf, sizeof(rnbuf), "%s", r.name.c_str());
if (ImGui::InputText("Name", rnbuf, sizeof(rnbuf))) {
r.name = rnbuf;
app.types_dirty = true;
}
ImVec4 rcol = abgr_to_imvec4_full(r.color ? r.color : 0xFF888888u);
if (ImGui::ColorEdit4("Color", (float*)&rcol,
ImGuiColorEditFlags_NoInputs
| ImGuiColorEditFlags_AlphaBar)) {
r.color = imvec4_to_abgr(rcol);
app.types_dirty = true;
}
int st_idx = (r.style <= EDGE_DOTTED) ? (int)r.style : 0;
if (ImGui::Combo("Style", &st_idx, k_style_names, k_style_count)) {
r.style = (uint8_t)st_idx;
app.types_dirty = true;
}
} else {
ImGui::TextDisabled("(no relation selected)");
}
ImGui::EndGroup();
ImGui::EndTabItem();
}
ImGui::EndTabBar();
}
// ---- Footer (Save / Reload) -------------------------------------------
ImGui::Separator();
if (!app.types_dirty) ImGui::BeginDisabled();
if (fn_ui::button(TI_DEVICE_FLOPPY " Save to types.yaml",
fn_ui::ButtonVariant::Primary)) {
app.want_types_save = true;
}
if (!app.types_dirty) ImGui::EndDisabled();
ImGui::SameLine();
if (fn_ui::button(TI_REFRESH " Reload from disk", fn_ui::ButtonVariant::Subtle)) {
app.want_types_reload = true;
}
ImGui::SameLine();
if (app.types_dirty) {
ImGui::TextColored(ImVec4(0.95f, 0.6f, 0.2f, 1.0f), "* unsaved changes");
} else {
ImGui::TextDisabled("clean");
}
if (!app.types_save_error.empty()) {
ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "%s",
app.types_save_error.c_str());
}
ImGui::End();
}
bool views_type_editor_delete_modal(AppState& app) {
if (!app.show_te_delete_modal) return false;
bool confirmed = false;
if (fn_ui::modal_dialog_begin("Delete type", &app.show_te_delete_modal,
ImVec2(440, 0))) {
const char* tname = "?";
const char* tkind = "?";
if (app.te_pending_delete_e >= 0
&& app.te_pending_delete_e < (int)app.types_draft.entities.size()) {
tname = app.types_draft.entities[app.te_pending_delete_e].name.c_str();
tkind = "entity";
} else if (app.te_pending_delete_r >= 0
&& app.te_pending_delete_r < (int)app.types_draft.relations.size()) {
tname = app.types_draft.relations[app.te_pending_delete_r].name.c_str();
tkind = "relation";
}
ImGui::Text("Eliminar %s type \"%s\"?", tkind, tname);
if (app.te_delete_use_count > 0) {
ImGui::TextColored(ImVec4(0.95f, 0.6f, 0.2f, 1.0f),
"Hay %d entidades en uso de este type_ref. Quedaran huerfanas hasta que las cambies.",
app.te_delete_use_count);
} else {
ImGui::TextDisabled("Ninguna entidad usa este tipo actualmente.");
}
ImGui::Spacing();
if (fn_ui::button("Delete", fn_ui::ButtonVariant::Primary)) {
if (app.te_pending_delete_e >= 0
&& app.te_pending_delete_e < (int)app.types_draft.entities.size()) {
app.types_draft.entities.erase(
app.types_draft.entities.begin() + app.te_pending_delete_e);
if (app.te_entity_idx >= (int)app.types_draft.entities.size())
app.te_entity_idx = (int)app.types_draft.entities.size() - 1;
} else if (app.te_pending_delete_r >= 0
&& app.te_pending_delete_r < (int)app.types_draft.relations.size()) {
app.types_draft.relations.erase(
app.types_draft.relations.begin() + app.te_pending_delete_r);
if (app.te_relation_idx >= (int)app.types_draft.relations.size())
app.te_relation_idx = (int)app.types_draft.relations.size() - 1;
}
app.te_pending_delete_e = -1;
app.te_pending_delete_r = -1;
app.te_delete_use_count = 0;
app.types_dirty = true;
app.show_te_delete_modal = false;
confirmed = true;
}
ImGui::SameLine();
if (fn_ui::button("Cancel", fn_ui::ButtonVariant::Subtle)) {
app.te_pending_delete_e = -1;
app.te_pending_delete_r = -1;
app.te_delete_use_count = 0;
app.show_te_delete_modal = false;
}
}
fn_ui::modal_dialog_end();
return confirmed;
}
} // namespace ge
+30
View File
@@ -137,6 +137,24 @@ struct AppState {
std::vector<std::string> insp_tag_suggestions;
std::vector<std::string> insp_type_options;
// ---- 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
// apply_types_yaml + rebuild de IconAtlas.
bool panel_type_editor = false;
ParsedTypes types_draft;
bool types_dirty = false;
int te_tab_idx = 0; // 0=Entities 1=Relations
int te_entity_idx = -1; // seleccion entity
int te_relation_idx = -1; // seleccion relation
bool want_types_save = false;
bool want_types_reload = false;
int te_pending_delete_e = -1; // entity idx pendiente de confirmar
int te_pending_delete_r = -1;
int te_delete_use_count = 0; // entidades afectadas
bool show_te_delete_modal = false;
std::string types_save_error; // mensaje a renderizar bajo Save
// ---- Filtros y busqueda FTS5 (issue 0009) ------------------------------
// Modos: 0 = highlight (no-match dimmed), 1 = hide (no-match invisible).
enum FilterMode { FM_HIGHLIGHT = 0, FM_HIDE = 1 };
@@ -205,6 +223,18 @@ EntityRecord views_inspector_build_record(const AppState& app);
// al cambiar de proyecto.
void views_inspector_clear_draft(AppState& app);
// ---- Type Editor (issue 0007) -------------------------------------------
// Renderiza el panel "Types" — tabs Entities/Relations, lista a la izquierda
// con +/-, panel de edicion a la derecha. Marca app.types_dirty al cambiar y
// activa app.want_types_save / app.want_types_reload desde el footer.
void views_type_editor(AppState& app);
// Modal de confirmacion para borrar un tipo en uso. Se abre cuando
// app.show_te_delete_modal = true. main.cpp es responsable de poblar
// te_delete_use_count via consulta a operations.db antes de mostrarlo.
bool views_type_editor_delete_modal(AppState& app);
// ---- Filter helpers (issue 0009) -----------------------------------------
// True si el filtro tiene query no vacia o al menos un tag activo.