data_table v1.3.0: Dots renderer for status timelines + fix dag_engine_ui antipattern + pitfalls doc (issue 0081-O.5)
PARTE A - CellRenderer::Dots (v1.3.0): - Add Dots=8 to CellRenderer enum (data_table_types.h) - Add dots_separator/dots_max/dots_show_count/dots_glyph_size fields to ColumnSpec - Implement draw_cell_custom case Dots in data_table.cpp - Parses comma-separated cell value into tokens - Looks up each token in badges for color + optional glyph override - Per-dot tooltip via tooltip_on_hover - tql_emit: serialize renderer="dots" + dots_max/dots_show_count/dots_glyph_size/dots_separator - tql_apply: deserialize all Dots fields - tql_emit_test: +6 assertions (58 total, 0 failed) - tql_apply_test: +8 assertions (114 total, 0 failed) - test_column_specs: +2 tests (10/10 pass) PARTE B - dag_engine_ui fix: 10 cols -> 6 cols (submodule commit 61314b7) PARTE C - docs/capabilities/data_table_renderers.md: - Update to v1.3.0 - Add decision tree for renderer selection - Add CellRenderer::Dots section with canonical example - Add Common pitfalls section (multiple columns, badge for free-text, etc.) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -130,6 +130,7 @@ enum class JoinStrategy { Left, Inner, Right, Full };
|
||||
// ----------------------------------------------------------------------------
|
||||
// CellRenderer: declarative rendering mode per column (issue 0081-N, v1.1.0).
|
||||
// Phase 2 (issue 0081-O, v1.2.0): Button=5 added.
|
||||
// Phase 2.5 (issue 0081-O.5, v1.3.0): Dots=8 added (inline status timeline).
|
||||
// ----------------------------------------------------------------------------
|
||||
enum class CellRenderer : uint8_t {
|
||||
Text = 0, // default — current behavior
|
||||
@@ -138,7 +139,8 @@ enum class CellRenderer : uint8_t {
|
||||
Duration = 3, // milliseconds with color gradient
|
||||
Icon = 4, // icon lookup by value string
|
||||
Button = 5, // clickable button; emits TableEvent::ButtonClick
|
||||
// Future (Phase 3): TextInput=6, Custom=7. IDs reserved.
|
||||
// 6, 7: reserved for Phase 3 (TextInput, Custom).
|
||||
Dots = 8, // inline dots sparkline; cell = separator-delimited tokens
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -203,6 +205,15 @@ struct ColumnSpec {
|
||||
// Tooltip (Phase 2, v1.2.0): per-cell hover tooltip
|
||||
std::string tooltip; // text; "auto" -> show cell value
|
||||
bool tooltip_on_hover = false; // if true, show on hover
|
||||
|
||||
// Dots (Phase 2.5, v1.3.0): CellRenderer::Dots — inline status sparkline.
|
||||
// Cell value is a separator-delimited string of tokens, e.g. "ok,error,ok".
|
||||
// Each token is looked up in `badges` for color (and optional glyph override via label).
|
||||
// Default glyph: u8"●"; override by setting BadgeRule.label to another UTF-8 glyph.
|
||||
char dots_separator = ','; // separator between tokens
|
||||
float dots_glyph_size = 0.0f; // glyph size px; 0 = default font size
|
||||
int dots_max = 0; // hard limit on dots shown; 0 = no limit
|
||||
bool dots_show_count = false; // if true, appends " (N)" after dots
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
@@ -540,11 +540,12 @@ ApplyResult apply(const std::string& lua_text,
|
||||
lua_getfield(L, -1, "renderer");
|
||||
if (lua_isstring(L, -1)) {
|
||||
std::string rn = lua_tostring(L, -1);
|
||||
if (rn == "badge") cs.renderer = data_table::CellRenderer::Badge;
|
||||
if (rn == "badge") cs.renderer = data_table::CellRenderer::Badge;
|
||||
else if (rn == "progress") cs.renderer = data_table::CellRenderer::Progress;
|
||||
else if (rn == "duration") cs.renderer = data_table::CellRenderer::Duration;
|
||||
else if (rn == "icon") cs.renderer = data_table::CellRenderer::Icon;
|
||||
else if (rn == "button") cs.renderer = data_table::CellRenderer::Button;
|
||||
else if (rn == "dots") cs.renderer = data_table::CellRenderer::Dots;
|
||||
else cs.renderer = data_table::CellRenderer::Text;
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
@@ -624,6 +625,23 @@ ApplyResult apply(const std::string& lua_text,
|
||||
if (lua_isstring(L, -1)) cs.button_color_hex = lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
|
||||
// Dots (Phase 2.5, v1.3.0)
|
||||
lua_getfield(L, -1, "dots_separator");
|
||||
if (lua_isstring(L, -1)) {
|
||||
const char* sep_s = lua_tostring(L, -1);
|
||||
if (sep_s && sep_s[0]) cs.dots_separator = sep_s[0];
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
lua_getfield(L, -1, "dots_max");
|
||||
if (lua_isnumber(L, -1)) cs.dots_max = (int)lua_tointeger(L, -1);
|
||||
lua_pop(L, 1);
|
||||
lua_getfield(L, -1, "dots_show_count");
|
||||
if (lua_isboolean(L, -1)) cs.dots_show_count = (bool)lua_toboolean(L, -1);
|
||||
lua_pop(L, 1);
|
||||
lua_getfield(L, -1, "dots_glyph_size");
|
||||
if (lua_isnumber(L, -1)) cs.dots_glyph_size = (float)lua_tonumber(L, -1);
|
||||
lua_pop(L, 1);
|
||||
|
||||
// Tooltip
|
||||
lua_getfield(L, -1, "tooltip");
|
||||
if (lua_isstring(L, -1)) cs.tooltip = lua_tostring(L, -1);
|
||||
|
||||
@@ -519,6 +519,40 @@ static void test_roundtrip_column_specs_tooltip() {
|
||||
check(cs2.tooltip_on_hover == true, "cs tooltip roundtrip: tooltip_on_hover=true");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test: column_specs roundtrip — Dots (v1.3.0)
|
||||
// ---------------------------------------------------------------------------
|
||||
static void test_roundtrip_column_specs_dots() {
|
||||
State st;
|
||||
st.stages.push_back(Stage{});
|
||||
|
||||
ColumnSpec cs;
|
||||
cs.id = "recent";
|
||||
cs.renderer = CellRenderer::Dots;
|
||||
cs.badges = {{"ok", "#22c55e", ""}, {"error", "#ef4444", "failed"}};
|
||||
cs.dots_max = 5;
|
||||
cs.dots_show_count = false;
|
||||
cs.tooltip_on_hover = true;
|
||||
st.aux_column_specs.push_back({cs});
|
||||
|
||||
std::vector<std::string> headers = {"name", "recent"};
|
||||
std::vector<ColumnType> types = {ColumnType::String, ColumnType::String};
|
||||
|
||||
std::string tql_text = tql::emit(st, headers, types);
|
||||
auto res = tql::apply(tql_text, headers);
|
||||
|
||||
check(res.ok, "cs dots roundtrip: ok");
|
||||
check(!res.state.aux_column_specs.empty() &&
|
||||
!res.state.aux_column_specs[0].empty(), "cs dots roundtrip: specs present");
|
||||
const auto& cs2 = res.state.aux_column_specs[0][0];
|
||||
check(cs2.renderer == CellRenderer::Dots, "cs dots roundtrip: renderer=Dots");
|
||||
check(cs2.dots_max == 5, "cs dots roundtrip: dots_max=5");
|
||||
check(cs2.tooltip_on_hover == true, "cs dots roundtrip: tooltip_on_hover=true");
|
||||
check(cs2.badges.size() == 2, "cs dots roundtrip: 2 badge rules");
|
||||
check(cs2.badges[0].value == "ok", "cs dots roundtrip: badge[0].value=ok");
|
||||
check(cs2.badges[1].label == "failed", "cs dots roundtrip: badge[1].label=failed");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// main
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -540,6 +574,7 @@ int main() {
|
||||
test_roundtrip_column_specs_badge();
|
||||
test_roundtrip_column_specs_button();
|
||||
test_roundtrip_column_specs_tooltip();
|
||||
test_roundtrip_column_specs_dots();
|
||||
|
||||
std::printf("---\nResults: %d passed, %d failed\n", g_pass, g_fail);
|
||||
return g_fail == 0 ? 0 : 1;
|
||||
|
||||
@@ -283,7 +283,8 @@ std::string emit(const State& state,
|
||||
// Emit the block only if at least one spec has a non-default renderer OR tooltip.
|
||||
bool any_renderable = false;
|
||||
for (const auto& cs : specs) {
|
||||
if (cs.renderer != data_table::CellRenderer::Text || cs.tooltip_on_hover) {
|
||||
if (cs.renderer != data_table::CellRenderer::Text || cs.tooltip_on_hover ||
|
||||
cs.renderer == data_table::CellRenderer::Dots) {
|
||||
any_renderable = true; break;
|
||||
}
|
||||
}
|
||||
@@ -301,6 +302,7 @@ std::string emit(const State& state,
|
||||
case data_table::CellRenderer::Duration: rname = "duration"; break;
|
||||
case data_table::CellRenderer::Icon: rname = "icon"; break;
|
||||
case data_table::CellRenderer::Button: rname = "button"; break;
|
||||
case data_table::CellRenderer::Dots: rname = "dots"; break;
|
||||
default: break;
|
||||
}
|
||||
out += ", renderer = " + lua_string_literal(rname);
|
||||
@@ -352,6 +354,22 @@ std::string emit(const State& state,
|
||||
if (!cs.button_color_hex.empty())
|
||||
out += ", button_color = " + lua_string_literal(cs.button_color_hex);
|
||||
}
|
||||
// Dots (Phase 2.5, v1.3.0)
|
||||
if (cs.renderer == data_table::CellRenderer::Dots) {
|
||||
if (cs.dots_separator != ',') {
|
||||
char sep_str[2] = {cs.dots_separator, '\0'};
|
||||
out += ", dots_separator = " + lua_string_literal(sep_str);
|
||||
}
|
||||
if (cs.dots_max > 0)
|
||||
out += ", dots_max = " + std::to_string(cs.dots_max);
|
||||
if (cs.dots_show_count)
|
||||
out += ", dots_show_count = true";
|
||||
if (cs.dots_glyph_size > 0.0f) {
|
||||
char buf[32];
|
||||
std::snprintf(buf, sizeof(buf), "%g", (double)cs.dots_glyph_size);
|
||||
out += std::string(", dots_glyph_size = ") + buf;
|
||||
}
|
||||
}
|
||||
// Tooltip
|
||||
if (cs.tooltip_on_hover) {
|
||||
out += ", tooltip = " + lua_string_literal(cs.tooltip.empty() ? "auto" : cs.tooltip);
|
||||
|
||||
@@ -306,6 +306,35 @@ static void test_emit_column_specs_tooltip() {
|
||||
check(contains(out, "tooltip_on_hover = true"), "emit column_specs tooltip: on_hover");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test: emit with Dots column_spec in aux_column_specs (v1.3.0)
|
||||
// ---------------------------------------------------------------------------
|
||||
static void test_emit_column_specs_dots() {
|
||||
State st;
|
||||
st.stages.push_back(Stage{});
|
||||
|
||||
ColumnSpec cs;
|
||||
cs.id = "recent";
|
||||
cs.renderer = CellRenderer::Dots;
|
||||
cs.badges = {{"ok", "#22c55e", ""}, {"error", "#ef4444", ""}};
|
||||
cs.dots_max = 5;
|
||||
cs.dots_show_count = false;
|
||||
cs.tooltip_on_hover = true;
|
||||
st.aux_column_specs.push_back({cs});
|
||||
|
||||
std::vector<std::string> headers = {"name", "recent"};
|
||||
std::vector<ColumnType> types = {ColumnType::String, ColumnType::String};
|
||||
|
||||
std::string out = tql::emit(st, headers, types);
|
||||
|
||||
check(contains(out, "renderer = \"dots\""), "emit column_specs dots: renderer");
|
||||
check(contains(out, "id = \"recent\""), "emit column_specs dots: id");
|
||||
check(contains(out, "dots_max = 5"), "emit column_specs dots: dots_max");
|
||||
check(contains(out, "tooltip_on_hover = true"), "emit column_specs dots: tooltip_on_hover");
|
||||
check(contains(out, "\"#22c55e\""), "emit column_specs dots: badge color ok");
|
||||
check(contains(out, "\"error\""), "emit column_specs dots: badge value error");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// main
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -323,6 +352,7 @@ int main() {
|
||||
test_emit_column_specs_badge();
|
||||
test_emit_column_specs_button();
|
||||
test_emit_column_specs_tooltip();
|
||||
test_emit_column_specs_dots();
|
||||
|
||||
std::printf("---\nResults: %d passed, %d failed\n", g_pass, g_fail);
|
||||
return g_fail == 0 ? 0 : 1;
|
||||
|
||||
Reference in New Issue
Block a user