feat(0036d): promote kind-aware (Group → clear group_id)
NodeGroups window kind=Group ahora expone un boton SmallButton(TI_ARROW_UP) por fila que saca la entidad del grupo (group_id = NULL) y dispara reload del grafo. kind=Table mantiene el comportamiento de issue 0011. - entity_ops: nueva op `entity_clear_group_id(db, id)` idempotente. Si la columna group_id no existe (BD pre-0035a) retorna true como no-op. Falla solo si la entidad no existe o SQLite revienta. - views.cpp: extra columna "promote" en kind=Group, tooltip header diferenciado por kind, boton conectado a app.want_clear_group_id_entity. - main.cpp: handler que ejecuta entity_clear_group_id, marca windows como dirty, llama reload_after_mutation y loguea `[node_groups] promoted X out of group`. - gx-cli: flag `node update --clear-group-id` (booleano) y exposicion MCP en inputSchema + MCP_DISPATCH defaults para que el agente Echo pueda promover via tool calls. - tests: 3 nuevos CLI (clear, idempotente, combinable con --name) y 4 MCP (defaults, schema, dispatch end-to-end, idempotente). WSL: 102 passed (95 base + 7). Windows: 91 passed, 11 skipped (84 base + 7). Refs: issues/0036d-promote-kind-aware.md
This commit is contained in:
@@ -199,6 +199,66 @@ bool entity_update_type(const char* db_path, const char* id, const char* new_typ
|
|||||||
return ok;
|
return ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool entity_clear_group_id(const char* db_path, const char* entity_id) {
|
||||||
|
if (!db_path || !entity_id || !*entity_id) return false;
|
||||||
|
sqlite3* db = nullptr;
|
||||||
|
if (sqlite3_open_v2(db_path, &db, SQLITE_OPEN_READWRITE, nullptr) != SQLITE_OK) {
|
||||||
|
if (db) sqlite3_close(db);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detectar si la columna `group_id` existe en `entities`. En BDs viejas
|
||||||
|
// (pre-issue 0035a) la columna no esta — el clear es no-op coherente:
|
||||||
|
// la entidad no puede pertenecer a un grupo si la columna no existe.
|
||||||
|
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* col = sqlite3_column_text(pst, 1);
|
||||||
|
if (col && std::strcmp((const char*)col, "group_id") == 0) {
|
||||||
|
has_group_id = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sqlite3_finalize(pst);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar que la entidad existe (independiente del path con/sin
|
||||||
|
// columna). Si no existe, devolvemos false (mismo contrato que
|
||||||
|
// entity_update_type).
|
||||||
|
{
|
||||||
|
sqlite3_stmt* est = nullptr;
|
||||||
|
if (sqlite3_prepare_v2(db, "SELECT 1 FROM entities WHERE id = ?", -1,
|
||||||
|
&est, nullptr) != SQLITE_OK) {
|
||||||
|
sqlite3_close(db);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
sqlite3_bind_text(est, 1, entity_id, -1, SQLITE_TRANSIENT);
|
||||||
|
bool exists = (sqlite3_step(est) == SQLITE_ROW);
|
||||||
|
sqlite3_finalize(est);
|
||||||
|
if (!exists) {
|
||||||
|
sqlite3_close(db);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!has_group_id) {
|
||||||
|
// No-op valido: la entidad existe pero no puede tener group_id.
|
||||||
|
sqlite3_close(db);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string ts = now_iso();
|
||||||
|
const char* p[2] = { ts.c_str(), entity_id };
|
||||||
|
bool ok = exec_one(db,
|
||||||
|
"UPDATE entities SET group_id = NULL, updated_at = ? WHERE id = ?",
|
||||||
|
p, 2);
|
||||||
|
sqlite3_close(db);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
bool entity_duplicate(const char* db_path, const char* id,
|
bool entity_duplicate(const char* db_path, const char* id,
|
||||||
char* out_id, size_t out_id_n)
|
char* out_id, size_t out_id_n)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -43,6 +43,18 @@ bool entity_delete(const char* db_path, const char* id);
|
|||||||
|
|
||||||
bool entity_update_type(const char* db_path, const char* id, const char* new_type);
|
bool entity_update_type(const char* db_path, const char* id, const char* new_type);
|
||||||
|
|
||||||
|
// Saca la entidad de su grupo: UPDATE entities SET group_id = NULL.
|
||||||
|
// Idempotente — si la entidad ya tenia group_id NULL, no falla. Devuelve
|
||||||
|
// true si la entidad existe (independientemente de si tenia o no grupo).
|
||||||
|
// Devuelve false si la entidad no existe o SQLite falla. En BDs antiguas
|
||||||
|
// sin la columna `group_id` retorna true como no-op (la entidad no puede
|
||||||
|
// pertenecer a un grupo si la columna no existe).
|
||||||
|
//
|
||||||
|
// Issue 0036d: pareja del flujo Promote en NodeGroups window cuando
|
||||||
|
// kind=Group — saca el nodo del grupo para que aparezca suelto en el
|
||||||
|
// canvas tras reload.
|
||||||
|
bool entity_clear_group_id(const char* db_path, const char* entity_id);
|
||||||
|
|
||||||
// Duplica una entidad existente. Mismo type/metadata, sufijo "_copy" en id
|
// Duplica una entidad existente. Mismo type/metadata, sufijo "_copy" en id
|
||||||
// y "(copia)" en name. Devuelve el nuevo id en out_id.
|
// y "(copia)" en name. Devuelve el nuevo id en out_id.
|
||||||
bool entity_duplicate(const char* db_path, const char* id,
|
bool entity_duplicate(const char* db_path, const char* id,
|
||||||
|
|||||||
@@ -230,6 +230,12 @@ def cmd_node_update(args) -> None:
|
|||||||
_die(f"bad tags: {e}")
|
_die(f"bad tags: {e}")
|
||||||
sets.append("tags = ?")
|
sets.append("tags = ?")
|
||||||
params.append(json.dumps(tags))
|
params.append(json.dumps(tags))
|
||||||
|
# Issue 0036d: --clear-group-id saca la entidad de su grupo
|
||||||
|
# (UPDATE entities SET group_id = NULL). Booleano sin value param;
|
||||||
|
# combinable con otros sets.
|
||||||
|
if getattr(args, "clear_group_id", False):
|
||||||
|
sets.append("group_id = NULL")
|
||||||
|
_log("node_update", f"clear_group_id=true id={args.id}")
|
||||||
if not sets:
|
if not sets:
|
||||||
_die("no fields to update")
|
_die("no fields to update")
|
||||||
|
|
||||||
@@ -829,7 +835,9 @@ MCP_TOOLS = [
|
|||||||
"notes": {"type": "string", "description": "Texto del panel Note. Reemplaza el contenido salvo que append_notes=true."},
|
"notes": {"type": "string", "description": "Texto del panel Note. Reemplaza el contenido salvo que append_notes=true."},
|
||||||
"append_notes": {"type": "boolean", "default": False,
|
"append_notes": {"type": "boolean", "default": False,
|
||||||
"description": "Si true, anyade `notes` al final con doble newline en vez de reemplazar."},
|
"description": "Si true, anyade `notes` al final con doble newline en vez de reemplazar."},
|
||||||
"tags": {"type": "string", "description": "JSON array literal o CSV 'a,b,c'"}},
|
"tags": {"type": "string", "description": "JSON array literal o CSV 'a,b,c'"},
|
||||||
|
"clear_group_id": {"type": "boolean", "default": False,
|
||||||
|
"description": "saca la entidad del grupo (group_id = NULL)"}},
|
||||||
"required": ["id"]}},
|
"required": ["id"]}},
|
||||||
{"name": "node_delete",
|
{"name": "node_delete",
|
||||||
"description": "Borra la entidad y todas sus relaciones. Irreversible. Confirmar con el usuario antes.",
|
"description": "Borra la entidad y todas sus relaciones. Irreversible. Confirmar con el usuario antes.",
|
||||||
@@ -931,7 +939,8 @@ MCP_DISPATCH = {
|
|||||||
"node_update": (cmd_node_update, {"name": None, "type": None,
|
"node_update": (cmd_node_update, {"name": None, "type": None,
|
||||||
"status": None, "description": None,
|
"status": None, "description": None,
|
||||||
"notes": None, "append_notes": False,
|
"notes": None, "append_notes": False,
|
||||||
"tags": None}),
|
"tags": None,
|
||||||
|
"clear_group_id": False}),
|
||||||
"node_delete": (cmd_node_delete, {}),
|
"node_delete": (cmd_node_delete, {}),
|
||||||
"rel_create": (cmd_rel_create, {"name": None}),
|
"rel_create": (cmd_rel_create, {"name": None}),
|
||||||
"rel_delete": (cmd_rel_delete, {}),
|
"rel_delete": (cmd_rel_delete, {}),
|
||||||
@@ -1086,6 +1095,11 @@ def main() -> None:
|
|||||||
"en vez de reemplazarlas (separador: doble newline).")
|
"en vez de reemplazarlas (separador: doble newline).")
|
||||||
sp.add_argument("--tags",
|
sp.add_argument("--tags",
|
||||||
help='JSON array o "tag1,tag2" CSV')
|
help='JSON array o "tag1,tag2" CSV')
|
||||||
|
sp.add_argument("--clear-group-id", dest="clear_group_id",
|
||||||
|
action="store_true",
|
||||||
|
help="saca la entidad del grupo (UPDATE group_id = NULL). "
|
||||||
|
"Idempotente; combinable con otros campos. "
|
||||||
|
"Issue 0036d.")
|
||||||
sp.set_defaults(fn=cmd_node_update)
|
sp.set_defaults(fn=cmd_node_update)
|
||||||
sp = n.add_parser("list")
|
sp = n.add_parser("list")
|
||||||
sp.add_argument("--type")
|
sp.add_argument("--type")
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
---
|
||||||
|
id: 0036d
|
||||||
|
title: Promote en NodeGroups ramificado por kind (Table row vs Group child)
|
||||||
|
status: done
|
||||||
|
priority: high
|
||||||
|
created: 2026-05-04
|
||||||
|
parent: 0036
|
||||||
|
depends_on: [0036b]
|
||||||
|
---
|
||||||
|
|
||||||
|
## Objetivo
|
||||||
|
|
||||||
|
Boton "Promote" por fila en la NodeGroups window. La accion se
|
||||||
|
ramifica por `kind`:
|
||||||
|
|
||||||
|
- **kind = Table** (DuckDB row): comportamiento actual del issue 0011
|
||||||
|
— INSERT entity nueva apuntando a la fila DuckDB. Idempotente: si
|
||||||
|
ya existe entidad para ese row_id, no duplica.
|
||||||
|
- **kind = Group** (entidad hija): UPDATE clear group_id. La entidad
|
||||||
|
sale del grupo y aparece como nodo libre en el canvas tras reload.
|
||||||
|
|
||||||
|
## Cambios
|
||||||
|
|
||||||
|
### Nueva op en `entity_ops` (Group promote)
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// Saca la entidad de su grupo (group_id = NULL). No-op si ya estaba
|
||||||
|
// fuera (group_id ya era NULL). Idempotente.
|
||||||
|
bool entity_clear_group_id(const char* db_path, const char* entity_id);
|
||||||
|
```
|
||||||
|
|
||||||
|
Tambien anyadir su pareja MCP en `gx-cli`:
|
||||||
|
|
||||||
|
```
|
||||||
|
gx-cli node update <id> --clear-group-id
|
||||||
|
```
|
||||||
|
|
||||||
|
(Para que el agente Echo pueda promover entidades del grupo via MCP.
|
||||||
|
Argumento booleano que dispara la op directa, sin pasar por el flujo
|
||||||
|
de --notes/--name.)
|
||||||
|
|
||||||
|
### Render del boton Promote
|
||||||
|
|
||||||
|
En la pintura de la window (en `views.cpp`), por cada fila visible
|
||||||
|
mostrar un `SmallButton(TI_ARROW_UP)` con tooltip:
|
||||||
|
|
||||||
|
- kind = Table: `"Promote row to entity"` — comportamiento existente.
|
||||||
|
- kind = Group: `"Promote out of group (move to canvas)"` — llama a
|
||||||
|
la nueva op.
|
||||||
|
|
||||||
|
Tras la accion, marcar `g_app.want_reload = true` para que el grafo
|
||||||
|
se refresque y la entidad reaparezca suelta.
|
||||||
|
|
||||||
|
### Tooltip del header
|
||||||
|
|
||||||
|
Cuando entras a la window, mostrar un texto suave indicando el
|
||||||
|
comportamiento del Promote segun kind. Una sola linea, color text
|
||||||
|
muted.
|
||||||
|
|
||||||
|
## Acceptance criteria
|
||||||
|
|
||||||
|
- Click promote en row de Group: la entidad pierde `group_id`
|
||||||
|
(verificable por SQL), reload del grafo la muestra suelta
|
||||||
|
colgando del source via la relacion existente.
|
||||||
|
- Click promote en row de Group ya promovida (group_id ya NULL):
|
||||||
|
no-op sin error.
|
||||||
|
- Click promote en row de Table: comportamiento como hoy. Sin
|
||||||
|
regresion en flow existente.
|
||||||
|
- Tooltip diferenciado entre kinds.
|
||||||
|
- Tests pytest:
|
||||||
|
- `test_entity_clear_group_id_removes_membership`
|
||||||
|
- `test_entity_clear_group_id_idempotent`
|
||||||
|
- `test_gx_cli_node_update_clear_group_id` (CLI)
|
||||||
|
- MCP regression: `clear_group_id` en defaults
|
||||||
|
|
||||||
|
## TBD
|
||||||
|
|
||||||
|
Branch `issue/0036d-promote-kind-aware`, merge `--no-ff` a master.
|
||||||
|
|
||||||
|
## Out of scope
|
||||||
|
|
||||||
|
- Promote masivo (multi-select) — fase 2.
|
||||||
|
- Re-agrupar por tipo (multi-select dentro de NodeGroups window) —
|
||||||
|
fase 2.
|
||||||
@@ -1819,6 +1819,20 @@ static void render() {
|
|||||||
g_app.want_demote_entity = false;
|
g_app.want_demote_entity = false;
|
||||||
g_app.demote_entity_id.clear();
|
g_app.demote_entity_id.clear();
|
||||||
}
|
}
|
||||||
|
// Issue 0036d: promote out-of-group (kind=Group de NodeGroups window).
|
||||||
|
if (g_app.want_clear_group_id_entity
|
||||||
|
&& !g_app.clear_group_id_entity_id.empty()
|
||||||
|
&& !g_input_path.empty()) {
|
||||||
|
if (ge::entity_clear_group_id(g_input_path.c_str(),
|
||||||
|
g_app.clear_group_id_entity_id.c_str())) {
|
||||||
|
std::fprintf(stdout, "[node_groups] promoted %s out of group\n",
|
||||||
|
g_app.clear_group_id_entity_id.c_str());
|
||||||
|
for (auto& kv : g_app.node_groups_windows) kv.second.page_dirty = true;
|
||||||
|
reload_after_mutation();
|
||||||
|
}
|
||||||
|
g_app.want_clear_group_id_entity = false;
|
||||||
|
g_app.clear_group_id_entity_id.clear();
|
||||||
|
}
|
||||||
if (g_app.want_focus_entity && !g_app.focus_entity_id.empty()) {
|
if (g_app.want_focus_entity && !g_app.focus_entity_id.empty()) {
|
||||||
for (int i = 0; i < g_graph.node_count; ++i) {
|
for (int i = 0; i < g_graph.node_count; ++i) {
|
||||||
const char* sid = ge::entity_index_lookup(
|
const char* sid = ge::entity_index_lookup(
|
||||||
|
|||||||
@@ -255,6 +255,57 @@ class TestCliNodeUpdate:
|
|||||||
"--name", "x", expect_ok=False)
|
"--name", "x", expect_ok=False)
|
||||||
assert out.get("ok") is False
|
assert out.get("ok") is False
|
||||||
|
|
||||||
|
def test_node_update_clear_group_id(self, env_dirs):
|
||||||
|
"""Issue 0036d: --clear-group-id saca la entidad del grupo."""
|
||||||
|
nid = self._make(env_dirs, name="grouped")
|
||||||
|
# Asignar group_id directamente via SQL (simula entity en grupo).
|
||||||
|
cn = sqlite3.connect(env_dirs["ops"])
|
||||||
|
cn.execute("UPDATE entities SET group_id = ? WHERE id = ?",
|
||||||
|
("group_xyz", nid))
|
||||||
|
cn.commit()
|
||||||
|
cn.close()
|
||||||
|
# Sanity: precondicion.
|
||||||
|
row = fetchone(env_dirs["ops"],
|
||||||
|
"SELECT group_id FROM entities WHERE id=?", nid)
|
||||||
|
assert row[0] == "group_xyz"
|
||||||
|
# Run clear.
|
||||||
|
run_gx(env_dirs, "node", "update", nid, "--clear-group-id")
|
||||||
|
row = fetchone(env_dirs["ops"],
|
||||||
|
"SELECT group_id FROM entities WHERE id=?", nid)
|
||||||
|
assert row[0] is None
|
||||||
|
|
||||||
|
def test_node_update_clear_group_id_idempotent(self, env_dirs):
|
||||||
|
"""Issue 0036d: clear sobre entidad ya sin grupo no falla."""
|
||||||
|
nid = self._make(env_dirs, name="solo")
|
||||||
|
# Precondicion: group_id NULL desde el start.
|
||||||
|
row = fetchone(env_dirs["ops"],
|
||||||
|
"SELECT group_id FROM entities WHERE id=?", nid)
|
||||||
|
assert row[0] is None
|
||||||
|
# Clear es no-op.
|
||||||
|
run_gx(env_dirs, "node", "update", nid, "--clear-group-id")
|
||||||
|
row = fetchone(env_dirs["ops"],
|
||||||
|
"SELECT group_id FROM entities WHERE id=?", nid)
|
||||||
|
assert row[0] is None
|
||||||
|
# Y la entidad sigue existiendo.
|
||||||
|
row = fetchone(env_dirs["ops"],
|
||||||
|
"SELECT id FROM entities WHERE id=?", nid)
|
||||||
|
assert row is not None and row[0] == nid
|
||||||
|
|
||||||
|
def test_node_update_clear_group_id_combinable(self, env_dirs):
|
||||||
|
"""--clear-group-id se combina con otros sets en un mismo update."""
|
||||||
|
nid = self._make(env_dirs, name="combo")
|
||||||
|
cn = sqlite3.connect(env_dirs["ops"])
|
||||||
|
cn.execute("UPDATE entities SET group_id = ? WHERE id = ?",
|
||||||
|
("group_abc", nid))
|
||||||
|
cn.commit()
|
||||||
|
cn.close()
|
||||||
|
run_gx(env_dirs, "node", "update", nid,
|
||||||
|
"--clear-group-id", "--name", "renamed")
|
||||||
|
row = fetchone(env_dirs["ops"],
|
||||||
|
"SELECT group_id, name FROM entities WHERE id=?", nid)
|
||||||
|
assert row[0] is None
|
||||||
|
assert row[1] == "renamed"
|
||||||
|
|
||||||
|
|
||||||
class TestCliNodeListShow:
|
class TestCliNodeListShow:
|
||||||
def test_node_show_returns_notes(self, env_dirs):
|
def test_node_show_returns_notes(self, env_dirs):
|
||||||
@@ -600,3 +651,49 @@ class TestMcpRegression0035d:
|
|||||||
props = tool["inputSchema"]["properties"]
|
props = tool["inputSchema"]["properties"]
|
||||||
assert "notes" in props
|
assert "notes" in props
|
||||||
assert "append_notes" in props
|
assert "append_notes" in props
|
||||||
|
|
||||||
|
|
||||||
|
class TestMcpRegression0036d:
|
||||||
|
"""Issue 0036d: clear_group_id como flag MCP del node_update.
|
||||||
|
Bloquea cualquier futura regresion donde el dispatcher pierda el
|
||||||
|
default o el inputSchema deje de anunciar el campo."""
|
||||||
|
|
||||||
|
def test_node_update_dispatch_includes_clear_group_id_default(self, gx_module):
|
||||||
|
_, defaults = gx_module.MCP_DISPATCH["node_update"]
|
||||||
|
assert "clear_group_id" in defaults
|
||||||
|
assert defaults["clear_group_id"] is False
|
||||||
|
|
||||||
|
def test_node_update_inputschema_advertises_clear_group_id(self, gx_module):
|
||||||
|
tool = next(t for t in gx_module.MCP_TOOLS
|
||||||
|
if t["name"] == "node_update")
|
||||||
|
props = tool["inputSchema"]["properties"]
|
||||||
|
assert "clear_group_id" in props
|
||||||
|
assert props["clear_group_id"]["type"] == "boolean"
|
||||||
|
assert props["clear_group_id"]["default"] is False
|
||||||
|
|
||||||
|
def test_mcp_dispatch_clear_group_id_clears_membership(self, gx_module,
|
||||||
|
mcp_env):
|
||||||
|
"""End-to-end via dispatcher: crear nodo, asignar grupo via SQL,
|
||||||
|
dispatch node_update con clear_group_id=True, verificar NULL."""
|
||||||
|
out = gx_module._mcp_dispatch("node_create",
|
||||||
|
{"name": "grp_member", "type": "text"})
|
||||||
|
nid = out["id"]
|
||||||
|
cn = sqlite3.connect(mcp_env["ops"])
|
||||||
|
cn.execute("UPDATE entities SET group_id = ? WHERE id = ?",
|
||||||
|
("group_mcp", nid))
|
||||||
|
cn.commit()
|
||||||
|
cn.close()
|
||||||
|
out = gx_module._mcp_dispatch("node_update",
|
||||||
|
{"id": nid, "clear_group_id": True})
|
||||||
|
assert out["ok"] is True
|
||||||
|
row = fetchone(mcp_env["ops"],
|
||||||
|
"SELECT group_id FROM entities WHERE id=?", nid)
|
||||||
|
assert row[0] is None
|
||||||
|
|
||||||
|
def test_mcp_dispatch_clear_group_id_idempotent(self, gx_module, mcp_env):
|
||||||
|
out = gx_module._mcp_dispatch("node_create",
|
||||||
|
{"name": "grp_solo", "type": "text"})
|
||||||
|
nid = out["id"]
|
||||||
|
out = gx_module._mcp_dispatch("node_update",
|
||||||
|
{"id": nid, "clear_group_id": True})
|
||||||
|
assert out["ok"] is True
|
||||||
|
|||||||
@@ -2025,6 +2025,13 @@ void views_node_groups_window(AppState& app) {
|
|||||||
m.duckdb_path.c_str(), m.table_name.c_str(),
|
m.duckdb_path.c_str(), m.table_name.c_str(),
|
||||||
(long long)w.total_rows);
|
(long long)w.total_rows);
|
||||||
}
|
}
|
||||||
|
// Issue 0036d: tooltip suave que explica el promote segun kind.
|
||||||
|
if (is_group) {
|
||||||
|
ImGui::TextDisabled("Promote: saca el nodo del grupo");
|
||||||
|
} else {
|
||||||
|
ImGui::TextDisabled(
|
||||||
|
"Promote: convierte fila DuckDB en entidad del grafo");
|
||||||
|
}
|
||||||
if (!w.last_error.empty()) {
|
if (!w.last_error.empty()) {
|
||||||
ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f),
|
ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f),
|
||||||
"ERROR: %s", w.last_error.c_str());
|
"ERROR: %s", w.last_error.c_str());
|
||||||
@@ -2033,10 +2040,11 @@ void views_node_groups_window(AppState& app) {
|
|||||||
|
|
||||||
// Tabla — layout depende del kind:
|
// Tabla — layout depende del kind:
|
||||||
// Table: [id_column] + columns[] + [promoted] (col_count = N+2)
|
// Table: [id_column] + columns[] + [promoted] (col_count = N+2)
|
||||||
// Group: columns[] (id, name, type_ref, status, updated_at)
|
// Group: columns[] + [promote] (col_count = N+1)
|
||||||
// (col_count = N)
|
// (issue 0036d: ultima columna lleva un boton TI_ARROW_UP
|
||||||
|
// para sacar la entidad del grupo.)
|
||||||
const int col_count = is_group
|
const int col_count = is_group
|
||||||
? (int)m.columns.size()
|
? (int)m.columns.size() + 1
|
||||||
: (int)m.columns.size() + 2;
|
: (int)m.columns.size() + 2;
|
||||||
ImGuiTableFlags tflags =
|
ImGuiTableFlags tflags =
|
||||||
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
|
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
|
||||||
@@ -2053,6 +2061,9 @@ void views_node_groups_window(AppState& app) {
|
|||||||
: ImGuiTableColumnFlags_WidthStretch,
|
: ImGuiTableColumnFlags_WidthStretch,
|
||||||
is_id ? 160.0f : 0.0f);
|
is_id ? 160.0f : 0.0f);
|
||||||
}
|
}
|
||||||
|
// 0036d: columna extra para el boton Promote-out-of-group.
|
||||||
|
ImGui::TableSetupColumn("promote",
|
||||||
|
ImGuiTableColumnFlags_WidthFixed, 60.0f);
|
||||||
} else {
|
} else {
|
||||||
ImGui::TableSetupColumn(m.id_column.empty() ? "id" : m.id_column.c_str(),
|
ImGui::TableSetupColumn(m.id_column.empty() ? "id" : m.id_column.c_str(),
|
||||||
ImGuiTableColumnFlags_WidthFixed, 100.0f);
|
ImGuiTableColumnFlags_WidthFixed, 100.0f);
|
||||||
@@ -2101,6 +2112,15 @@ void views_node_groups_window(AppState& app) {
|
|||||||
if (c < row.values.size())
|
if (c < row.values.size())
|
||||||
ImGui::TextUnformatted(row.values[c].c_str());
|
ImGui::TextUnformatted(row.values[c].c_str());
|
||||||
}
|
}
|
||||||
|
// 0036d: boton Promote-out-of-group en la ultima columna.
|
||||||
|
ImGui::TableSetColumnIndex(col_count - 1);
|
||||||
|
if (ImGui::SmallButton(TI_ARROW_UP "##promote_grp")) {
|
||||||
|
app.want_clear_group_id_entity = true;
|
||||||
|
app.clear_group_id_entity_id = row.id;
|
||||||
|
}
|
||||||
|
if (ImGui::IsItemHovered()) {
|
||||||
|
ImGui::SetTooltip("Promote out of group (move to canvas)");
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// kind=Table — comportamiento original (DuckDB-backed).
|
// kind=Table — comportamiento original (DuckDB-backed).
|
||||||
bool is_promoted = !row.promoted_entity_id.empty();
|
bool is_promoted = !row.promoted_entity_id.empty();
|
||||||
|
|||||||
@@ -200,6 +200,11 @@ struct AppState {
|
|||||||
bool want_focus_entity = false; // tras promote+open inspector
|
bool want_focus_entity = false; // tras promote+open inspector
|
||||||
std::string focus_entity_id;
|
std::string focus_entity_id;
|
||||||
|
|
||||||
|
// Issue 0036d: Promote en NodeGroups kind=Group → saca la entidad
|
||||||
|
// del grupo (group_id = NULL). main.cpp lo procesa y dispara reload.
|
||||||
|
bool want_clear_group_id_entity = false;
|
||||||
|
std::string clear_group_id_entity_id;
|
||||||
|
|
||||||
// Modal "Import dataset..." (issue 0011 Ingesta).
|
// Modal "Import dataset..." (issue 0011 Ingesta).
|
||||||
bool show_import_modal = false;
|
bool show_import_modal = false;
|
||||||
char import_path_buf[512] = {};
|
char import_path_buf[512] = {};
|
||||||
|
|||||||
Reference in New Issue
Block a user