From 6287231ceef96b0bebf89f3e3836ab21fcc30d2e Mon Sep 17 00:00:00 2001 From: reugenio Date: Wed, 17 Dec 2025 17:26:53 +0100 Subject: [PATCH] feat: AdvancedTable Fases 7-8 - Lookup & Callbacks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/widgets/advanced_table/advanced_table.zig | 175 ++++++++++++++++++ src/widgets/advanced_table/state.zig | 13 ++ 2 files changed, 188 insertions(+) diff --git a/src/widgets/advanced_table/advanced_table.zig b/src/widgets/advanced_table/advanced_table.zig index a4ef48a..0147454 100644 --- a/src/widgets/advanced_table/advanced_table.zig +++ b/src/widgets/advanced_table/advanced_table.zig @@ -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()); +} diff --git a/src/widgets/advanced_table/state.zig b/src/widgets/advanced_table/state.zig index e4dfb95..f896c6a 100644 --- a/src/widgets/advanced_table/state.zig +++ b/src/widgets/advanced_table/state.zig @@ -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, };