Bug: Tab within injected row was triggering immediate row commit. This happened because RowIdGetter.getRowId() returned the REAL row ID for the injected row index, instead of NEW_ROW_ID. Fix: - RowIdGetter now takes injected_idx parameter - Returns NEW_ROW_ID for injected row - Adjusts indices for rows after injection (row-1) - num_rows calculation now accounts for injection (+1) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
237 lines
9.2 KiB
Zig
237 lines
9.2 KiB
Zig
//! PagedDataSource - Adaptador de TableDataSource para datos paginados
|
|
//!
|
|
//! Wrappea VirtualAdvancedTableState + DataProvider para implementar la interfaz
|
|
//! TableDataSource de table_core. Permite unificar el pattern de renderizado
|
|
//! entre AdvancedTable (memoria) y VirtualAdvancedTable (paginado).
|
|
|
|
const std = @import("std");
|
|
const table_core = @import("../table_core.zig");
|
|
const state_mod = @import("state.zig");
|
|
const types = @import("types.zig");
|
|
const data_provider_mod = @import("data_provider.zig");
|
|
|
|
const VirtualAdvancedTableState = state_mod.VirtualAdvancedTableState;
|
|
const ColumnDef = types.ColumnDef;
|
|
const RowData = types.RowData;
|
|
const DataProvider = data_provider_mod.DataProvider;
|
|
const TableDataSource = table_core.TableDataSource;
|
|
|
|
// =============================================================================
|
|
// PagedDataSource
|
|
// =============================================================================
|
|
|
|
/// Adaptador que implementa TableDataSource para datos paginados (ventana virtual).
|
|
/// Lee valores desde current_window en el state, que es propiedad del DataProvider.
|
|
pub const PagedDataSource = struct {
|
|
/// Referencia al estado de la tabla (contiene current_window)
|
|
state: *VirtualAdvancedTableState,
|
|
|
|
/// DataProvider subyacente (para getRowId cuando fuera de ventana)
|
|
provider: ?DataProvider,
|
|
|
|
/// Definiciones de columnas (para validar índices)
|
|
columns: []const ColumnDef,
|
|
|
|
const Self = @This();
|
|
|
|
/// Crea un nuevo PagedDataSource
|
|
pub fn init(
|
|
state: *VirtualAdvancedTableState,
|
|
columns: []const ColumnDef,
|
|
provider: ?DataProvider,
|
|
) Self {
|
|
return .{
|
|
.state = state,
|
|
.columns = columns,
|
|
.provider = provider,
|
|
};
|
|
}
|
|
|
|
// =========================================================================
|
|
// Implementación de TableDataSource
|
|
// =========================================================================
|
|
|
|
/// Retorna el número total de filas (filtered count + inyección)
|
|
pub fn getRowCount(self: *Self) usize {
|
|
// Usar conteo filtrado si está disponible
|
|
const count_info = self.state.getDisplayCount();
|
|
var count = count_info.value;
|
|
|
|
// Si hay una fila inyectada, sumar 1 al conteo visual
|
|
if (self.state.injected_row_idx != null) {
|
|
count += 1;
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
/// Escribe el valor de una celda en el buffer proporcionado.
|
|
/// El row es índice global, se convierte a índice de ventana.
|
|
/// Maneja filas inyectadas (Ctrl+N entre líneas).
|
|
/// Retorna slice del buffer con el contenido.
|
|
pub fn getCellValueInto(self: *Self, row: usize, col: usize, buf: []u8) []const u8 {
|
|
// Validar columna
|
|
if (col >= self.columns.len) return "";
|
|
|
|
// =====================================================================
|
|
// Manejo de fila inyectada
|
|
// =====================================================================
|
|
if (self.state.injected_row_idx) |inj_idx| {
|
|
if (row == inj_idx) {
|
|
// Esta es la fila inyectada - leer del edit buffer
|
|
if (self.state.row_edit_buffer.getPendingValue(col)) |pending| {
|
|
std.debug.print("[PDS-DEBUG] injected row={} col={} pending=\"{s}\"\n", .{ row, col, pending });
|
|
const copy_len = @min(pending.len, buf.len);
|
|
@memcpy(buf[0..copy_len], pending[0..copy_len]);
|
|
return buf[0..copy_len];
|
|
}
|
|
// Sin valor pendiente - retornar vacío (celda nueva)
|
|
std.debug.print("[PDS-DEBUG] injected row={} col={} NO pending\n", .{ row, col });
|
|
return "";
|
|
} else if (row > inj_idx) {
|
|
// Fila después de la inyección - ajustar índice (-1)
|
|
return self.getCellValueFromProvider(row - 1, col, buf);
|
|
}
|
|
// row < inj_idx: continuar normal
|
|
}
|
|
|
|
// Flujo normal (sin inyección o row < inj_idx)
|
|
return self.getCellValueFromProvider(row, col, buf);
|
|
}
|
|
|
|
/// Helper interno: obtiene valor de celda del provider (sin ajuste por inyección)
|
|
fn getCellValueFromProvider(self: *Self, row: usize, col: usize, buf: []u8) []const u8 {
|
|
// Convertir índice global a índice de ventana
|
|
const window_idx = self.state.globalToWindowIndex(row) orelse {
|
|
// Fila fuera de ventana actual - retornar vacío
|
|
return "";
|
|
};
|
|
|
|
// Obtener fila de la ventana
|
|
if (window_idx >= self.state.current_window.len) return "";
|
|
const row_data = self.state.current_window[window_idx];
|
|
|
|
// Obtener valor de la columna
|
|
if (col >= row_data.values.len) return "";
|
|
const value = row_data.values[col];
|
|
|
|
// Copiar al buffer proporcionado
|
|
const copy_len = @min(value.len, buf.len);
|
|
@memcpy(buf[0..copy_len], value[0..copy_len]);
|
|
return buf[0..copy_len];
|
|
}
|
|
|
|
/// Retorna el ID único de una fila.
|
|
/// Si está en ventana, usa window data. Si no, consulta al provider.
|
|
/// Maneja filas inyectadas (Ctrl+N entre líneas).
|
|
pub fn getRowId(self: *Self, row: usize) i64 {
|
|
// Manejo de fila inyectada
|
|
if (self.state.injected_row_idx) |inj_idx| {
|
|
if (row == inj_idx) {
|
|
// Esta es la fila inyectada
|
|
return self.state.injected_row_id;
|
|
} else if (row > inj_idx) {
|
|
// Fila después de la inyección - ajustar índice
|
|
return self.getRowIdFromProvider(row - 1);
|
|
}
|
|
}
|
|
|
|
// Flujo normal
|
|
return self.getRowIdFromProvider(row);
|
|
}
|
|
|
|
/// Helper interno: obtiene ID de fila del provider (sin ajuste por inyección)
|
|
fn getRowIdFromProvider(self: *Self, row: usize) i64 {
|
|
// Intentar obtener de la ventana
|
|
if (self.state.globalToWindowIndex(row)) |window_idx| {
|
|
if (window_idx < self.state.current_window.len) {
|
|
return self.state.current_window[window_idx].id;
|
|
}
|
|
}
|
|
|
|
// Fallback al provider si está disponible
|
|
if (self.provider) |provider| {
|
|
return provider.getRowId(row) orelse -1;
|
|
}
|
|
|
|
// Si no hay provider, usar índice como fallback
|
|
return @intCast(row);
|
|
}
|
|
|
|
/// Verifica si una celda es editable.
|
|
/// TODO: Podría usar metadata de columnas cuando esté disponible.
|
|
pub fn isCellEditable(self: *Self, row: usize, col: usize) bool {
|
|
_ = row;
|
|
// Por ahora, todas las columnas son no editables por defecto
|
|
// La edición se maneja a nivel de VirtualAdvancedTable
|
|
if (col >= self.columns.len) return false;
|
|
return false; // VirtualAdvancedTable maneja edición de forma específica
|
|
}
|
|
|
|
/// Invalida cache.
|
|
/// Delega al state para forzar refetch.
|
|
pub fn invalidate(self: *Self) void {
|
|
self.state.invalidateWindow();
|
|
}
|
|
|
|
// =========================================================================
|
|
// Conversión a TableDataSource
|
|
// =========================================================================
|
|
|
|
/// Crea TableDataSource vtable para este adaptador
|
|
pub fn toDataSource(self: *Self) TableDataSource {
|
|
return table_core.makeTableDataSource(Self, self);
|
|
}
|
|
};
|
|
|
|
// =============================================================================
|
|
// Tests
|
|
// =============================================================================
|
|
|
|
test "PagedDataSource basic" {
|
|
// Test básico de que compila y los tipos son correctos
|
|
const columns = [_]ColumnDef{
|
|
.{ .name = "id", .title = "ID", .width = 50 },
|
|
.{ .name = "name", .title = "Name", .width = 200 },
|
|
};
|
|
|
|
var state = VirtualAdvancedTableState{};
|
|
var ds = PagedDataSource.init(&state, &columns, null);
|
|
const tds = ds.toDataSource();
|
|
|
|
// Sin datos, row count es 0
|
|
try std.testing.expectEqual(@as(usize, 0), tds.getRowCount());
|
|
}
|
|
|
|
test "PagedDataSource getCellValueInto" {
|
|
const columns = [_]ColumnDef{
|
|
.{ .name = "id", .title = "ID", .width = 50 },
|
|
.{ .name = "name", .title = "Name", .width = 200 },
|
|
};
|
|
|
|
// Crear datos de prueba
|
|
const values1 = [_][]const u8{ "1", "Alice" };
|
|
const values2 = [_][]const u8{ "2", "Bob" };
|
|
const rows = [_]RowData{
|
|
.{ .id = 100, .values = &values1 },
|
|
.{ .id = 101, .values = &values2 },
|
|
};
|
|
|
|
var state = VirtualAdvancedTableState{};
|
|
state.current_window = &rows;
|
|
state.window_start = 10; // Ventana empieza en índice global 10
|
|
state.filtered_count = .{ .value = 100, .state = .ready };
|
|
|
|
var ds = PagedDataSource.init(&state, &columns, null);
|
|
|
|
// Buffer para valores
|
|
var buf: [256]u8 = undefined;
|
|
|
|
// Fila en ventana (global 10 = window 0)
|
|
const val1 = ds.getCellValueInto(10, 1, &buf);
|
|
try std.testing.expectEqualStrings("Alice", val1);
|
|
|
|
// Fila fuera de ventana
|
|
const val2 = ds.getCellValueInto(0, 0, &buf);
|
|
try std.testing.expectEqualStrings("", val2);
|
|
}
|