feat: AdvancedTable Fases 7-8 - Lookup & Callbacks
Fase 7 - Lookup & Auto-fill: - performLookupAndAutoFill() en commitEdit - Busca en DataStore.lookup() al editar columna lookup - Auto-rellena columnas según auto_fill_columns mapping - Indicador visual "?" en header para columnas lookup - Campo lookup_success en AdvancedTableResult Fase 8 - Callbacks + Debounce: - invokeCallbacks() con sistema de debounce (150ms default) - on_row_selected: al cambiar selección - on_cell_changed: al confirmar edición - on_active_row_changed: al cambiar de fila (para paneles detalle) - Campos last_callback_time_ms, last_notified_row en state Tests: 373/373 (+3 nuevos) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
2dccddeab0
commit
6287231cee
2 changed files with 188 additions and 0 deletions
|
|
@ -189,9 +189,61 @@ pub fn advancedTableRect(
|
|||
}
|
||||
}
|
||||
|
||||
// Phase 8: Invoke callbacks
|
||||
invokeCallbacks(ctx, table_state, table_schema, &result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Callback System (Phase 8)
|
||||
// =============================================================================
|
||||
|
||||
fn invokeCallbacks(
|
||||
ctx: *Context,
|
||||
table_state: *AdvancedTableState,
|
||||
table_schema: *const TableSchema,
|
||||
result: *AdvancedTableResult,
|
||||
) void {
|
||||
const config = table_schema.config;
|
||||
const current_time = ctx.current_time_ms;
|
||||
|
||||
// Check debounce
|
||||
const time_since_last = current_time -| table_state.last_callback_time_ms;
|
||||
const debounce_ok = time_since_last >= config.callback_debounce_ms;
|
||||
|
||||
// on_row_selected: called when selection changes (with debounce)
|
||||
if (result.selection_changed and debounce_ok) {
|
||||
if (table_schema.on_row_selected) |callback| {
|
||||
if (table_state.selected_row >= 0) {
|
||||
const row_idx: usize = @intCast(table_state.selected_row);
|
||||
if (table_state.getRowConst(row_idx)) |row| {
|
||||
callback(row_idx, row);
|
||||
table_state.last_callback_time_ms = current_time;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// on_active_row_changed: called when moving to a different row (for loading detail panels)
|
||||
// Only fires once per row change, not on every frame
|
||||
if (table_state.selected_row != table_state.last_notified_row) {
|
||||
if (table_schema.on_active_row_changed) |callback| {
|
||||
if (table_state.selected_row >= 0) {
|
||||
const new_row_idx: usize = @intCast(table_state.selected_row);
|
||||
if (table_state.getRowConst(new_row_idx)) |row| {
|
||||
const old_row: ?usize = if (table_state.last_notified_row >= 0)
|
||||
@intCast(table_state.last_notified_row)
|
||||
else
|
||||
null;
|
||||
callback(old_row, new_row_idx, row);
|
||||
}
|
||||
}
|
||||
}
|
||||
table_state.last_notified_row = table_state.selected_row;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Internal Rendering
|
||||
// =============================================================================
|
||||
|
|
@ -244,6 +296,12 @@ fn drawHeader(
|
|||
const text_y = header_y + @as(i32, @intCast((config.header_height - 8) / 2));
|
||||
ctx.pushCommand(Command.text(col_x + 4, text_y, col.title, colors.header_fg));
|
||||
|
||||
// Draw lookup indicator (Phase 7c) - small "?" icon
|
||||
if (col.hasLookup()) {
|
||||
const lookup_x = col_x + @as(i32, @intCast(col.width)) - 24;
|
||||
ctx.pushCommand(Command.text(lookup_x, text_y, "?", Style.Color.primary));
|
||||
}
|
||||
|
||||
// Draw sort indicator
|
||||
if (table_state.sort_column == @as(i32, @intCast(idx))) {
|
||||
const indicator_x = col_x + @as(i32, @intCast(col.width)) - 12;
|
||||
|
|
@ -861,6 +919,65 @@ fn commitEdit(
|
|||
row.set(col_def.name, new_value) catch {};
|
||||
table_state.markDirty(row_idx);
|
||||
result.cell_edited = true;
|
||||
|
||||
// Lookup & Auto-fill (Phase 7)
|
||||
if (col_def.hasLookup()) {
|
||||
performLookupAndAutoFill(table_state, table_schema, row, col_def, new_value, result);
|
||||
}
|
||||
|
||||
// Call on_cell_changed callback (Phase 8)
|
||||
if (table_schema.on_cell_changed) |callback| {
|
||||
const old_value = table_state.original_value orelse CellValue{ .null_val = {} };
|
||||
callback(row_idx, col_idx, old_value, new_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Perform lookup in related table and auto-fill columns
|
||||
fn performLookupAndAutoFill(
|
||||
table_state: *AdvancedTableState,
|
||||
table_schema: *const TableSchema,
|
||||
row: *Row,
|
||||
col_def: *const ColumnDef,
|
||||
lookup_value: CellValue,
|
||||
result: *AdvancedTableResult,
|
||||
) void {
|
||||
// Need DataStore to perform lookup
|
||||
const data_store = table_schema.data_store orelse return;
|
||||
|
||||
// Get lookup configuration
|
||||
const lookup_table = col_def.lookup_table orelse return;
|
||||
const lookup_key = col_def.lookup_key_column orelse return;
|
||||
|
||||
// Perform lookup
|
||||
const lookup_result = data_store.lookup(
|
||||
lookup_table,
|
||||
lookup_key,
|
||||
lookup_value,
|
||||
table_state.allocator,
|
||||
) catch return;
|
||||
|
||||
// If lookup found a match, auto-fill related columns
|
||||
if (lookup_result) |lookup_row| {
|
||||
defer {
|
||||
// Clean up the lookup row after use
|
||||
var mutable_lookup = lookup_row;
|
||||
mutable_lookup.deinit();
|
||||
}
|
||||
|
||||
// Auto-fill columns based on mapping
|
||||
if (col_def.auto_fill_columns) |mappings| {
|
||||
for (mappings) |mapping| {
|
||||
const source_value = lookup_row.get(mapping.source_field);
|
||||
if (!source_value.isEmpty()) {
|
||||
row.set(mapping.target_column, source_value) catch {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.lookup_success = true;
|
||||
} else {
|
||||
result.lookup_success = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1020,3 +1137,61 @@ test "blendColor" {
|
|||
try std.testing.expectEqual(@as(u8, 127), gray.g);
|
||||
try std.testing.expectEqual(@as(u8, 127), gray.b);
|
||||
}
|
||||
|
||||
test "AdvancedTableResult lookup_success field" {
|
||||
var result = AdvancedTableResult{};
|
||||
|
||||
// Default is null (no lookup attempted)
|
||||
try std.testing.expect(result.lookup_success == null);
|
||||
|
||||
// Can be set to true (lookup found)
|
||||
result.lookup_success = true;
|
||||
try std.testing.expect(result.lookup_success.? == true);
|
||||
|
||||
// Can be set to false (lookup not found)
|
||||
result.lookup_success = false;
|
||||
try std.testing.expect(result.lookup_success.? == false);
|
||||
}
|
||||
|
||||
test "AdvancedTableState callback fields" {
|
||||
var table_state = AdvancedTableState.init(std.testing.allocator);
|
||||
defer table_state.deinit();
|
||||
|
||||
// Initial state
|
||||
try std.testing.expectEqual(@as(u64, 0), table_state.last_callback_time_ms);
|
||||
try std.testing.expectEqual(@as(i32, -1), table_state.last_notified_row);
|
||||
|
||||
// Can be updated
|
||||
table_state.last_callback_time_ms = 1000;
|
||||
table_state.last_notified_row = 5;
|
||||
|
||||
try std.testing.expectEqual(@as(u64, 1000), table_state.last_callback_time_ms);
|
||||
try std.testing.expectEqual(@as(i32, 5), table_state.last_notified_row);
|
||||
}
|
||||
|
||||
test "ColumnDef hasLookup" {
|
||||
// Column without lookup
|
||||
const col_no_lookup = ColumnDef{
|
||||
.name = "test",
|
||||
.title = "Test",
|
||||
};
|
||||
try std.testing.expect(!col_no_lookup.hasLookup());
|
||||
|
||||
// Column with lookup enabled but no config
|
||||
const col_partial = ColumnDef{
|
||||
.name = "test",
|
||||
.title = "Test",
|
||||
.enable_lookup = true,
|
||||
};
|
||||
try std.testing.expect(!col_partial.hasLookup()); // Missing table/key
|
||||
|
||||
// Column with full lookup config
|
||||
const col_full = ColumnDef{
|
||||
.name = "test",
|
||||
.title = "Test",
|
||||
.enable_lookup = true,
|
||||
.lookup_table = "other_table",
|
||||
.lookup_key_column = "id",
|
||||
};
|
||||
try std.testing.expect(col_full.hasLookup());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -124,6 +124,16 @@ pub const AdvancedTableState = struct {
|
|||
/// Does this table have focus
|
||||
focused: bool = false,
|
||||
|
||||
// =========================================================================
|
||||
// Callbacks & Debounce (Phase 8)
|
||||
// =========================================================================
|
||||
|
||||
/// Last time a callback was invoked (for debouncing)
|
||||
last_callback_time_ms: u64 = 0,
|
||||
|
||||
/// Last row that triggered on_active_row_changed (to avoid duplicate calls)
|
||||
last_notified_row: i32 = -1,
|
||||
|
||||
// =========================================================================
|
||||
// Lifecycle
|
||||
// =========================================================================
|
||||
|
|
@ -671,6 +681,9 @@ pub const AdvancedTableResult = struct {
|
|||
crud_action: ?CRUDAction = null,
|
||||
crud_success: bool = true,
|
||||
|
||||
// Lookup (Phase 7)
|
||||
lookup_success: ?bool = null, // null = no lookup, true = found, false = not found
|
||||
|
||||
// Focus
|
||||
clicked: bool = false,
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue