feat(table_core): RowEditBuffer + commit al abandonar fila (Excel-style)
- RowEditBuffer: acumula cambios de fila antes de commit - checkRowChangeAndCommit(): detecta cambio fila + commit automático - buildCommitInfo(), isGhostRow(), NEW_ROW_ID - VirtualAdvancedTableState: row_edit_buffer field - VirtualAdvancedTableResult: row_committed, row_changes[], etc. - Comportamiento: Tab entre celdas acumula, cambiar fila hace commit
This commit is contained in:
parent
0026dbff2a
commit
d16019d54f
3 changed files with 261 additions and 24 deletions
|
|
@ -371,6 +371,187 @@ pub fn handleEditingKeyboard(
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Edición de fila completa (commit al abandonar fila, estilo Excel)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/// Máximo de columnas soportadas para cambios pendientes
|
||||||
|
pub const MAX_PENDING_COLUMNS: usize = 32;
|
||||||
|
|
||||||
|
/// Máximo tamaño de valor por celda
|
||||||
|
pub const MAX_CELL_VALUE_LEN: usize = 256;
|
||||||
|
|
||||||
|
/// ID especial para filas nuevas (ghost row)
|
||||||
|
pub const NEW_ROW_ID: i64 = -1;
|
||||||
|
|
||||||
|
/// Cambio pendiente en una columna
|
||||||
|
pub const PendingCellChange = struct {
|
||||||
|
/// Índice de columna
|
||||||
|
col: usize,
|
||||||
|
/// Valor nuevo (slice al buffer interno)
|
||||||
|
value: []const u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Buffer para acumular cambios de una fila antes de commit
|
||||||
|
/// Usado por los states de los widgets, procesado por funciones de table_core
|
||||||
|
pub const RowEditBuffer = struct {
|
||||||
|
/// ID de la fila siendo editada (NEW_ROW_ID si es ghost row)
|
||||||
|
row_id: i64 = NEW_ROW_ID,
|
||||||
|
|
||||||
|
/// Índice de fila (para navegación)
|
||||||
|
row_index: usize = 0,
|
||||||
|
|
||||||
|
/// Es una fila nueva (ghost row que el usuario está rellenando)
|
||||||
|
is_new_row: bool = false,
|
||||||
|
|
||||||
|
/// Hay cambios pendientes
|
||||||
|
has_changes: bool = false,
|
||||||
|
|
||||||
|
/// Buffers de valores por columna (almacenamiento fijo)
|
||||||
|
value_buffers: [MAX_PENDING_COLUMNS][MAX_CELL_VALUE_LEN]u8 = undefined,
|
||||||
|
|
||||||
|
/// Longitudes de cada valor
|
||||||
|
value_lens: [MAX_PENDING_COLUMNS]usize = [_]usize{0} ** MAX_PENDING_COLUMNS,
|
||||||
|
|
||||||
|
/// Flags: qué columnas tienen cambios
|
||||||
|
changed_cols: [MAX_PENDING_COLUMNS]bool = [_]bool{false} ** MAX_PENDING_COLUMNS,
|
||||||
|
|
||||||
|
/// Número de columnas con cambios
|
||||||
|
change_count: usize = 0,
|
||||||
|
|
||||||
|
/// Inicializa/resetea el buffer para una nueva fila
|
||||||
|
pub fn startEdit(self: *RowEditBuffer, row_id: i64, row_index: usize, is_new: bool) void {
|
||||||
|
self.row_id = row_id;
|
||||||
|
self.row_index = row_index;
|
||||||
|
self.is_new_row = is_new;
|
||||||
|
self.has_changes = false;
|
||||||
|
self.change_count = 0;
|
||||||
|
for (0..MAX_PENDING_COLUMNS) |i| {
|
||||||
|
self.changed_cols[i] = false;
|
||||||
|
self.value_lens[i] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Añade un cambio pendiente para una columna
|
||||||
|
pub fn addChange(self: *RowEditBuffer, col: usize, value: []const u8) void {
|
||||||
|
if (col >= MAX_PENDING_COLUMNS) return;
|
||||||
|
|
||||||
|
// Copiar valor al buffer interno
|
||||||
|
const len = @min(value.len, MAX_CELL_VALUE_LEN);
|
||||||
|
@memcpy(self.value_buffers[col][0..len], value[0..len]);
|
||||||
|
self.value_lens[col] = len;
|
||||||
|
|
||||||
|
// Marcar como cambiado
|
||||||
|
if (!self.changed_cols[col]) {
|
||||||
|
self.changed_cols[col] = true;
|
||||||
|
self.change_count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.has_changes = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Obtiene el valor pendiente de una columna (si hay cambio)
|
||||||
|
pub fn getPendingValue(self: *const RowEditBuffer, col: usize) ?[]const u8 {
|
||||||
|
if (col >= MAX_PENDING_COLUMNS) return null;
|
||||||
|
if (!self.changed_cols[col]) return null;
|
||||||
|
return self.value_buffers[col][0..self.value_lens[col]];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Limpia el buffer (después de commit o discard)
|
||||||
|
pub fn clear(self: *RowEditBuffer) void {
|
||||||
|
self.row_id = NEW_ROW_ID;
|
||||||
|
self.row_index = 0;
|
||||||
|
self.is_new_row = false;
|
||||||
|
self.has_changes = false;
|
||||||
|
self.change_count = 0;
|
||||||
|
for (0..MAX_PENDING_COLUMNS) |i| {
|
||||||
|
self.changed_cols[i] = false;
|
||||||
|
self.value_lens[i] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Información para hacer commit de los cambios de una fila
|
||||||
|
/// Retornada cuando el usuario abandona una fila editada
|
||||||
|
pub const RowCommitInfo = struct {
|
||||||
|
/// ID de la fila (NEW_ROW_ID si es INSERT)
|
||||||
|
row_id: i64,
|
||||||
|
|
||||||
|
/// Es INSERT (nueva fila) o UPDATE (fila existente)
|
||||||
|
is_insert: bool,
|
||||||
|
|
||||||
|
/// Lista de cambios (columna, valor)
|
||||||
|
changes: []const PendingCellChange,
|
||||||
|
|
||||||
|
/// Número de cambios
|
||||||
|
change_count: usize,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Construye la info de commit desde un RowEditBuffer
|
||||||
|
/// El caller debe proveer el array para almacenar los cambios
|
||||||
|
pub fn buildCommitInfo(
|
||||||
|
buffer: *const RowEditBuffer,
|
||||||
|
changes_out: []PendingCellChange,
|
||||||
|
) ?RowCommitInfo {
|
||||||
|
if (!buffer.has_changes) return null;
|
||||||
|
|
||||||
|
var count: usize = 0;
|
||||||
|
for (0..MAX_PENDING_COLUMNS) |col| {
|
||||||
|
if (buffer.changed_cols[col] and count < changes_out.len) {
|
||||||
|
changes_out[count] = .{
|
||||||
|
.col = col,
|
||||||
|
.value = buffer.value_buffers[col][0..buffer.value_lens[col]],
|
||||||
|
};
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return RowCommitInfo{
|
||||||
|
.row_id = buffer.row_id,
|
||||||
|
.is_insert = buffer.is_new_row,
|
||||||
|
.changes = changes_out[0..count],
|
||||||
|
.change_count = count,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verifica si hay que hacer commit antes de editar nueva celda.
|
||||||
|
/// Si la fila cambió y hay cambios pendientes, retorna commit info.
|
||||||
|
/// Siempre inicializa el buffer para la nueva fila.
|
||||||
|
///
|
||||||
|
/// Uso típico en widget:
|
||||||
|
/// ```
|
||||||
|
/// if (table_core.checkRowChangeAndCommit(&state.row_edit_buffer, new_id, new_idx, is_ghost, &changes)) |info| {
|
||||||
|
/// result.row_committed = true;
|
||||||
|
/// result.commit_info = info;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub fn checkRowChangeAndCommit(
|
||||||
|
buffer: *RowEditBuffer,
|
||||||
|
new_row_id: i64,
|
||||||
|
new_row_index: usize,
|
||||||
|
is_new_row: bool,
|
||||||
|
changes_out: []PendingCellChange,
|
||||||
|
) ?RowCommitInfo {
|
||||||
|
// Si es la misma fila, no hacer nada
|
||||||
|
if (buffer.row_id == new_row_id) return null;
|
||||||
|
|
||||||
|
// Si hay cambios pendientes en la fila anterior, construir commit
|
||||||
|
var commit_info: ?RowCommitInfo = null;
|
||||||
|
if (buffer.has_changes) {
|
||||||
|
commit_info = buildCommitInfo(buffer, changes_out);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iniciar edición de la nueva fila
|
||||||
|
buffer.startEdit(new_row_id, new_row_index, is_new_row);
|
||||||
|
|
||||||
|
return commit_info;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verifica si un row_id corresponde a la ghost row (fila nueva)
|
||||||
|
pub fn isGhostRow(row_id: i64) bool {
|
||||||
|
return row_id == NEW_ROW_ID;
|
||||||
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Utilidades
|
// Utilidades
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
|
||||||
|
|
@ -177,6 +177,13 @@ pub const VirtualAdvancedTableState = struct {
|
||||||
/// Flag: celda requiere commit al terminar edición
|
/// Flag: celda requiere commit al terminar edición
|
||||||
cell_value_changed: bool = false,
|
cell_value_changed: bool = false,
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Buffer de cambios de fila (commit al abandonar fila, estilo Excel)
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/// Buffer para acumular cambios de la fila actual antes de commit
|
||||||
|
row_edit_buffer: table_core.RowEditBuffer = .{},
|
||||||
|
|
||||||
const Self = @This();
|
const Self = @This();
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
@ -713,6 +720,7 @@ pub const VirtualAdvancedTableState = struct {
|
||||||
self.edit_buffer_len = 0;
|
self.edit_buffer_len = 0;
|
||||||
self.edit_cursor = 0;
|
self.edit_cursor = 0;
|
||||||
self.cell_value_changed = false;
|
self.cell_value_changed = false;
|
||||||
|
self.row_edit_buffer.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
|
||||||
|
|
@ -85,28 +85,30 @@ pub const VirtualAdvancedTableResult = struct {
|
||||||
clicked: bool = false,
|
clicked: bool = false,
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Edición CRUD Excel-style
|
// Edición CRUD Excel-style (commit al abandonar fila)
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
/// Una celda fue modificada (el usuario hizo commit con Enter/Tab)
|
/// Una fila fue completada (el usuario cambió de fila, tenía cambios pendientes)
|
||||||
cell_committed: bool = false,
|
/// Cuando es true, row_commit_id y row_changes contienen los datos
|
||||||
|
row_committed: bool = false,
|
||||||
|
|
||||||
/// La fila cambió de edición (auto-save requerido)
|
/// ID de la fila que se hizo commit (NEW_ROW_ID = -1 para inserts)
|
||||||
row_changed: bool = false,
|
row_commit_id: i64 = table_core.NEW_ROW_ID,
|
||||||
|
|
||||||
|
/// Es un INSERT (ghost row) o UPDATE (fila existente)
|
||||||
|
row_commit_is_insert: bool = false,
|
||||||
|
|
||||||
|
/// Cambios de la fila (válidos si row_committed = true)
|
||||||
|
/// Buffer estático que sobrevive el frame
|
||||||
|
row_changes: [table_core.MAX_PENDING_COLUMNS]table_core.PendingCellChange = undefined,
|
||||||
|
|
||||||
|
/// Número de cambios en row_changes
|
||||||
|
row_changes_count: usize = 0,
|
||||||
|
|
||||||
/// El usuario canceló edición (Escape 2x = descartar fila)
|
/// El usuario canceló edición (Escape 2x = descartar fila)
|
||||||
row_discarded: bool = false,
|
row_discarded: bool = false,
|
||||||
|
|
||||||
/// Celda que fue editada (si cell_committed = true)
|
/// Navegación solicitada después de edición
|
||||||
edited_cell: ?CellId = null,
|
|
||||||
|
|
||||||
/// Nuevo valor de la celda (si cell_committed = true)
|
|
||||||
edited_value: ?[]const u8 = null,
|
|
||||||
|
|
||||||
/// Fila anterior (si row_changed = true, para auto-save)
|
|
||||||
previous_row: ?usize = null,
|
|
||||||
|
|
||||||
/// Navegación solicitada después de commit
|
|
||||||
navigate_direction: cell_editor.NavigateDirection = .none,
|
navigate_direction: cell_editor.NavigateDirection = .none,
|
||||||
|
|
||||||
/// Tab presionado sin edición activa (pasar focus al siguiente widget)
|
/// Tab presionado sin edición activa (pasar focus al siguiente widget)
|
||||||
|
|
@ -114,6 +116,26 @@ pub const VirtualAdvancedTableResult = struct {
|
||||||
|
|
||||||
/// Shift estaba presionado con Tab (para tab_out inverso)
|
/// Shift estaba presionado con Tab (para tab_out inverso)
|
||||||
tab_shift: bool = false,
|
tab_shift: bool = false,
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Compatibilidad (DEPRECADO - usar row_committed)
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/// @deprecated: Usar row_committed. Mantenido para compatibilidad.
|
||||||
|
cell_committed: bool = false,
|
||||||
|
/// @deprecated
|
||||||
|
row_changed: bool = false,
|
||||||
|
/// @deprecated
|
||||||
|
edited_cell: ?CellId = null,
|
||||||
|
/// @deprecated
|
||||||
|
edited_value: ?[]const u8 = null,
|
||||||
|
/// @deprecated
|
||||||
|
previous_row: ?usize = null,
|
||||||
|
|
||||||
|
/// Obtiene los cambios como slice (helper para compatibilidad)
|
||||||
|
pub fn getRowChanges(self: *const VirtualAdvancedTableResult) []const table_core.PendingCellChange {
|
||||||
|
return self.row_changes[0..self.row_changes_count];
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
@ -287,26 +309,25 @@ pub fn virtualAdvancedTableRect(
|
||||||
const edited_cell = list_state.editing_cell.?;
|
const edited_cell = list_state.editing_cell.?;
|
||||||
const new_value = list_state.getEditText();
|
const new_value = list_state.getEditText();
|
||||||
|
|
||||||
// Check if row changed (for auto-save)
|
// Añadir cambio al buffer de fila (NO commit inmediato)
|
||||||
if (list_state.last_edited_row) |last_row| {
|
// El commit real se hace cuando el usuario abandona la fila
|
||||||
if (edited_cell.row != last_row and list_state.row_dirty) {
|
if (list_state.hasValueChanged()) {
|
||||||
result.row_changed = true;
|
list_state.row_edit_buffer.addChange(edited_cell.col, new_value);
|
||||||
result.previous_row = last_row;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Commit the edit
|
// Compatibilidad: mantener flags antiguos
|
||||||
if (list_state.commitEdit()) {
|
|
||||||
result.cell_committed = true;
|
result.cell_committed = true;
|
||||||
result.edited_cell = edited_cell;
|
result.edited_cell = edited_cell;
|
||||||
result.edited_value = new_value;
|
result.edited_value = new_value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Finalizar edición de celda (sin commit a BD)
|
||||||
|
_ = list_state.commitEdit();
|
||||||
result.navigate_direction = editor_result.navigate;
|
result.navigate_direction = editor_result.navigate;
|
||||||
} else if (editor_result.escaped) {
|
} else if (editor_result.escaped) {
|
||||||
const action = list_state.handleEscape();
|
const action = list_state.handleEscape();
|
||||||
if (action == .discard_row) {
|
if (action == .discard_row) {
|
||||||
result.row_discarded = true;
|
result.row_discarded = true;
|
||||||
|
list_state.row_edit_buffer.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -373,6 +394,33 @@ pub fn virtualAdvancedTableRect(
|
||||||
result.double_click_id = list_state.selected_id;
|
result.double_click_id = list_state.selected_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Commit de fila al cambiar de selección
|
||||||
|
// =========================================================================
|
||||||
|
// Si la selección cambió y hay cambios pendientes en otra fila, hacer commit
|
||||||
|
if (list_state.selection_changed and list_state.row_edit_buffer.has_changes) {
|
||||||
|
const new_row_id = list_state.selected_id orelse table_core.NEW_ROW_ID;
|
||||||
|
const new_row_idx = list_state.getSelectedRow() orelse 0;
|
||||||
|
const is_ghost = table_core.isGhostRow(new_row_id);
|
||||||
|
|
||||||
|
// checkRowChangeAndCommit compara row_ids y hace commit si son diferentes
|
||||||
|
if (table_core.checkRowChangeAndCommit(
|
||||||
|
&list_state.row_edit_buffer,
|
||||||
|
new_row_id,
|
||||||
|
new_row_idx,
|
||||||
|
is_ghost,
|
||||||
|
&result.row_changes,
|
||||||
|
)) |commit_info| {
|
||||||
|
result.row_committed = true;
|
||||||
|
result.row_commit_id = commit_info.row_id;
|
||||||
|
result.row_commit_is_insert = commit_info.is_insert;
|
||||||
|
result.row_changes_count = commit_info.change_count;
|
||||||
|
|
||||||
|
// Compatibilidad
|
||||||
|
result.row_changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue