//! AdvancedTable Schema - Column and table definitions //! //! Schema-driven configuration for AdvancedTable. //! Defines columns, their types, and behaviors. const std = @import("std"); const types = @import("types.zig"); pub const CellValue = types.CellValue; pub const ColumnType = types.ColumnType; pub const RowLockState = types.RowLockState; pub const Row = types.Row; pub const ValidationResult = types.ValidationResult; pub const TableColors = types.TableColors; pub const TableConfig = types.TableConfig; pub const ValidatorFn = types.ValidatorFn; pub const FormatterFn = types.FormatterFn; pub const ParserFn = types.ParserFn; pub const GetRowLockStateFn = types.GetRowLockStateFn; pub const OnRowSelectedFn = types.OnRowSelectedFn; pub const OnCellChangedFn = types.OnCellChangedFn; pub const OnActiveRowChangedFn = types.OnActiveRowChangedFn; // ============================================================================= // Auto-Fill Mapping // ============================================================================= /// Mapping for auto-fill after lookup pub const AutoFillMapping = struct { /// Field name in lookup table source_field: []const u8, /// Column name in this table to fill target_column: []const u8, }; // ============================================================================= // Column Alignment // ============================================================================= /// Text alignment for column content pub const ColumnAlign = enum { left, center, right, }; // ============================================================================= // Column Definition // ============================================================================= /// Complete column definition pub const ColumnDef = struct { /// Internal name (key in row data) name: []const u8, /// Display title in header title: []const u8, /// Width in pixels width: u32 = 100, /// Data type column_type: ColumnType = .string, /// Is editable editable: bool = true, /// Is sortable sortable: bool = true, /// Minimum width when resizing min_width: u32 = 40, /// Maximum width (0 = unlimited) max_width: u32 = 0, /// Text alignment text_align: ColumnAlign = .left, /// Is visible (can be hidden via config) visible: bool = true, // ========================================================================= // Lookup Configuration // ========================================================================= /// Enable database lookup enable_lookup: bool = false, /// Table name for lookup lookup_table: ?[]const u8 = null, /// Column in lookup table to match against lookup_key_column: ?[]const u8 = null, /// Columns to auto-fill after successful lookup auto_fill_columns: ?[]const AutoFillMapping = null, // ========================================================================= // Callbacks (per-column, optional) // ========================================================================= /// Custom validator for this column validator: ?ValidatorFn = null, /// Custom formatter for display formatter: ?FormatterFn = null, /// Custom parser for text input parser: ?ParserFn = null, // ========================================================================= // Select options (for ColumnType.select) // ========================================================================= /// Options for select dropdown select_options: ?[]const SelectOption = null, // ========================================================================= // Helper Methods // ========================================================================= /// Get effective width (clamped to min/max) pub fn getEffectiveWidth(self: *const ColumnDef, requested: u32) u32 { var w = requested; if (w < self.min_width) w = self.min_width; if (self.max_width > 0 and w > self.max_width) w = self.max_width; return w; } /// Check if this column can be edited pub fn canEdit(self: *const ColumnDef) bool { return self.editable and self.visible; } /// Check if this column has lookup enabled pub fn hasLookup(self: *const ColumnDef) bool { return self.enable_lookup and self.lookup_table != null and self.lookup_key_column != null; } }; /// Option for select dropdown pub const SelectOption = struct { /// Value stored in data value: CellValue, /// Display label label: []const u8, }; // ============================================================================= // DataStore Interface // ============================================================================= /// Interface for data persistence (database, file, API, etc.) pub const DataStore = struct { ptr: *anyopaque, vtable: *const VTable, pub const VTable = struct { /// Load all rows from data source load: *const fn (ptr: *anyopaque, allocator: std.mem.Allocator) anyerror!std.ArrayList(Row), /// Save row (INSERT or UPDATE depending on ID) save: *const fn (ptr: *anyopaque, row: *Row) anyerror!void, /// Delete row delete: *const fn (ptr: *anyopaque, row: *Row) anyerror!void, /// Lookup in related table lookup: *const fn (ptr: *anyopaque, table: []const u8, key_column: []const u8, key_value: CellValue, allocator: std.mem.Allocator) anyerror!?Row, }; /// Load all rows pub fn load(self: DataStore, allocator: std.mem.Allocator) !std.ArrayList(Row) { return self.vtable.load(self.ptr, allocator); } /// Save row (INSERT or UPDATE) pub fn save(self: DataStore, row: *Row) !void { return self.vtable.save(self.ptr, row); } /// Delete row pub fn delete(self: DataStore, row: *Row) !void { return self.vtable.delete(self.ptr, row); } /// Lookup in related table pub fn lookup(self: DataStore, table: []const u8, key_column: []const u8, key_value: CellValue, allocator: std.mem.Allocator) !?Row { return self.vtable.lookup(self.ptr, table, key_column, key_value, allocator); } }; // ============================================================================= // Table Schema // ============================================================================= /// Complete schema definition for AdvancedTable pub const TableSchema = struct { /// Table name (for DataStore operations) table_name: []const u8, /// Column definitions columns: []const ColumnDef, /// Configuration config: TableConfig = .{}, /// Colors (optional override) colors: ?*const TableColors = null, /// DataStore for persistence (optional) data_store: ?DataStore = null, // ========================================================================= // Global Callbacks // ========================================================================= /// Called when row is selected on_row_selected: ?OnRowSelectedFn = null, /// Called when cell value changes on_cell_changed: ?OnCellChangedFn = null, /// Called when active row changes (for loading detail panels) on_active_row_changed: ?OnActiveRowChangedFn = null, /// Called to get row lock state get_row_lock_state: ?GetRowLockStateFn = null, // ========================================================================= // Helper Methods // ========================================================================= /// Get column by name pub fn getColumn(self: *const TableSchema, name: []const u8) ?*const ColumnDef { for (self.columns) |*col| { if (std.mem.eql(u8, col.name, name)) { return col; } } return null; } /// Get column index by name pub fn getColumnIndex(self: *const TableSchema, name: []const u8) ?usize { for (self.columns, 0..) |col, i| { if (std.mem.eql(u8, col.name, name)) { return i; } } return null; } /// Get visible column count pub fn getVisibleColumnCount(self: *const TableSchema) usize { var count: usize = 0; for (self.columns) |col| { if (col.visible) count += 1; } return count; } /// Get total width of all visible columns pub fn getTotalWidth(self: *const TableSchema) u32 { var total: u32 = 0; for (self.columns) |col| { if (col.visible) total += col.width; } return total; } /// Find first editable column pub fn getFirstEditableColumn(self: *const TableSchema) ?usize { for (self.columns, 0..) |col, i| { if (col.canEdit()) { return i; } } return null; } /// Find next editable column after given index pub fn getNextEditableColumn(self: *const TableSchema, after: usize) ?usize { var i = after + 1; while (i < self.columns.len) : (i += 1) { if (self.columns[i].canEdit()) { return i; } } return null; } /// Find previous editable column before given index pub fn getPrevEditableColumn(self: *const TableSchema, before: usize) ?usize { if (before == 0) return null; var i = before - 1; while (true) { if (self.columns[i].canEdit()) { return i; } if (i == 0) break; i -= 1; } return null; } }; // ============================================================================= // Tests // ============================================================================= test "ColumnDef getEffectiveWidth" { const col = ColumnDef{ .name = "test", .title = "Test", .width = 100, .min_width = 50, .max_width = 200, }; try std.testing.expectEqual(@as(u32, 100), col.getEffectiveWidth(100)); try std.testing.expectEqual(@as(u32, 50), col.getEffectiveWidth(30)); // clamped to min try std.testing.expectEqual(@as(u32, 200), col.getEffectiveWidth(300)); // clamped to max } test "ColumnDef canEdit" { const editable = ColumnDef{ .name = "a", .title = "A", .editable = true, .visible = true }; const not_editable = ColumnDef{ .name = "b", .title = "B", .editable = false, .visible = true }; const hidden = ColumnDef{ .name = "c", .title = "C", .editable = true, .visible = false }; try std.testing.expect(editable.canEdit()); try std.testing.expect(!not_editable.canEdit()); try std.testing.expect(!hidden.canEdit()); } test "TableSchema getColumn" { const columns = [_]ColumnDef{ .{ .name = "id", .title = "ID" }, .{ .name = "name", .title = "Name" }, .{ .name = "value", .title = "Value" }, }; const schema = TableSchema{ .table_name = "test", .columns = &columns, }; const name_col = schema.getColumn("name"); try std.testing.expect(name_col != null); try std.testing.expectEqualStrings("Name", name_col.?.title); const unknown_col = schema.getColumn("unknown"); try std.testing.expect(unknown_col == null); } test "TableSchema getFirstEditableColumn" { const columns = [_]ColumnDef{ .{ .name = "id", .title = "ID", .editable = false }, .{ .name = "name", .title = "Name", .editable = true }, .{ .name = "value", .title = "Value", .editable = true }, }; const schema = TableSchema{ .table_name = "test", .columns = &columns, }; const first = schema.getFirstEditableColumn(); try std.testing.expectEqual(@as(?usize, 1), first); }