From ef34de4b3dcf20b3e94e70d3ba2647eb326a3c82 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Thu, 15 Jan 2026 16:55:31 +0700 Subject: [PATCH 1/5] feat: Add editable right sidebar with multi-row support - Implement editable fields in right sidebar for table rows - Add multi-row editing with 'Multiple values' support - Support SQL functions (NOW(), CURRENT_TIMESTAMP, etc.) - Add native macOS form styling with TextField and Menu - Include immediate save on Cmd+S - Handle NULL, DEFAULT, and SQL function values - Preserve edit state during data refresh to prevent UI flashing New files: - MultiRowEditState.swift for edit state management - EditableFieldView.swift for reusable field components Modified files: - RightSidebarView.swift with editable/readonly modes - MainContentView.swift with save handlers and SQL generation - MainContentCoordinator.swift with transaction support - MainContentView+Bindings.swift with sidebar state helpers --- TablePro/Models/MultiRowEditState.swift | 177 +++++++++++++++++ .../Extensions/MainContentView+Bindings.swift | 20 +- .../Views/Main/MainContentCoordinator.swift | 32 ++++ TablePro/Views/MainContentView.swift | 177 ++++++++++++++++- .../RightSidebar/EditableFieldView.swift | 180 ++++++++++++++++++ .../Views/RightSidebar/RightSidebarView.swift | 121 +++++++----- 6 files changed, 659 insertions(+), 48 deletions(-) create mode 100644 TablePro/Models/MultiRowEditState.swift create mode 100644 TablePro/Views/RightSidebar/EditableFieldView.swift diff --git a/TablePro/Models/MultiRowEditState.swift b/TablePro/Models/MultiRowEditState.swift new file mode 100644 index 000000000..fa26fcf31 --- /dev/null +++ b/TablePro/Models/MultiRowEditState.swift @@ -0,0 +1,177 @@ +// +// MultiRowEditState.swift +// TablePro +// +// State management for multi-row editing in right sidebar. +// Tracks pending edits across multiple selected rows. +// + +import Foundation +import Combine + +/// Represents the edit state for a single field across multiple rows +struct FieldEditState { + let columnIndex: Int + let columnName: String + let columnType: String + + /// Original values from all selected rows (nil if multiple different values) + let originalValue: String? + + /// Flag indicating if selected rows have different values for this field + let hasMultipleValues: Bool + + /// Pending new value (nil if not edited yet) + var pendingValue: String? + + /// Whether user has explicitly set this field to NULL + var isPendingNull: Bool + + /// Whether user has explicitly set this field to DEFAULT + var isPendingDefault: Bool + + var hasEdit: Bool { + pendingValue != nil || isPendingNull || isPendingDefault + } + + var effectiveValue: String? { + if isPendingDefault { + return "__DEFAULT__" + } else if isPendingNull { + return nil + } else { + return pendingValue + } + } +} + +/// Manages edit state for multi-row editing in sidebar +@MainActor +class MultiRowEditState: ObservableObject { + @Published var fields: [FieldEditState] = [] + + private(set) var selectedRowIndices: Set = [] + private(set) var allRows: [[String?]] = [] + private(set) var columns: [String] = [] + private(set) var columnTypes: [String] = [] + + var hasEdits: Bool { + fields.contains { $0.hasEdit } + } + + /// Configure state for the given selection + func configure( + selectedRowIndices: Set, + allRows: [[String?]], + columns: [String], + columnTypes: [String] + ) { + // Check if the underlying data has changed (not just edits) + let dataChanged = self.allRows != allRows || self.columns != columns + + self.selectedRowIndices = selectedRowIndices + self.allRows = allRows + self.columns = columns + self.columnTypes = columnTypes + + // Build field states + var newFields: [FieldEditState] = [] + + for (colIndex, columnName) in columns.enumerated() { + let columnType = colIndex < columnTypes.count ? columnTypes[colIndex] : "string" + + // Gather values from all selected rows + var values: [String?] = [] + for row in allRows { + let value = colIndex < row.count ? row[colIndex] : nil + values.append(value) + } + + // Check if all values are the same + let uniqueValues = Set(values.map { $0 ?? "__NULL__" }) + let hasMultipleValues = uniqueValues.count > 1 + + let originalValue: String? + if hasMultipleValues { + originalValue = nil + } else { + // Get first value, unwrapping the optional properly + originalValue = values.first.flatMap { $0 } + } + + // Preserve pending edits if data hasn't changed + var pendingValue: String? = nil + var isPendingNull = false + var isPendingDefault = false + + if !dataChanged, colIndex < fields.count { + let oldField = fields[colIndex] + pendingValue = oldField.pendingValue + isPendingNull = oldField.isPendingNull + isPendingDefault = oldField.isPendingDefault + } + + newFields.append(FieldEditState( + columnIndex: colIndex, + columnName: columnName, + columnType: columnType, + originalValue: originalValue, + hasMultipleValues: hasMultipleValues, + pendingValue: pendingValue, + isPendingNull: isPendingNull, + isPendingDefault: isPendingDefault + )) + } + + self.fields = newFields + } + + /// Update a field's pending value + func updateField(at index: Int, value: String?) { + guard index < fields.count else { return } + fields[index].pendingValue = value + fields[index].isPendingNull = false + fields[index].isPendingDefault = false + } + + /// Set a field to NULL + func setFieldToNull(at index: Int) { + guard index < fields.count else { return } + fields[index].pendingValue = nil + fields[index].isPendingNull = true + fields[index].isPendingDefault = false + } + + /// Set a field to DEFAULT + func setFieldToDefault(at index: Int) { + guard index < fields.count else { return } + fields[index].pendingValue = nil + fields[index].isPendingNull = false + fields[index].isPendingDefault = true + } + + /// Set a field to a SQL function (e.g., NOW()) + func setFieldToFunction(at index: Int, function: String) { + guard index < fields.count else { return } + fields[index].pendingValue = function + fields[index].isPendingNull = false + fields[index].isPendingDefault = false + } + + /// Clear all pending edits + func clearEdits() { + for i in 0.. [(columnIndex: Int, columnName: String, newValue: String?)] { + fields.compactMap { field in + guard field.hasEdit else { return nil } + return (field.columnIndex, field.columnName, field.effectiveValue) + } + } +} diff --git a/TablePro/Views/Main/Extensions/MainContentView+Bindings.swift b/TablePro/Views/Main/Extensions/MainContentView+Bindings.swift index 015c6f88a..cc6f25f6c 100644 --- a/TablePro/Views/Main/Extensions/MainContentView+Bindings.swift +++ b/TablePro/Views/Main/Extensions/MainContentView+Bindings.swift @@ -23,12 +23,30 @@ extension MainContentView { for (i, col) in tab.resultColumns.enumerated() { let value = i < row.values.count ? row.values[i] : nil - let type = "string" // Can be enhanced with actual column type info + let type = i < tab.columnTypes.count ? tab.columnTypes[i].displayName : "string" data.append((column: col, value: value, type: type)) } return data } + + // MARK: - Sidebar Edit State + + /// Determine if sidebar should be in editable mode + var isSidebarEditable: Bool { + guard let tab = coordinator.tabManager.selectedTab, + tab.tabType == .table, + !selectedRowIndices.isEmpty else { + return false + } + return true + } + + /// Check if selected row is deleted + var isSelectedRowDeleted: Bool { + guard let firstIndex = selectedRowIndices.min() else { return false } + return coordinator.changeManager.isRowDeleted(firstIndex) + } // MARK: - Sort State Binding diff --git a/TablePro/Views/Main/MainContentCoordinator.swift b/TablePro/Views/Main/MainContentCoordinator.swift index 9785c0e7e..bf2fa8b6e 100644 --- a/TablePro/Views/Main/MainContentCoordinator.swift +++ b/TablePro/Views/Main/MainContentCoordinator.swift @@ -1134,6 +1134,38 @@ final class MainContentCoordinator: ObservableObject { // MARK: - Table Creation + /// Execute sidebar changes immediately (single transaction) + func executeSidebarChanges(statements: [String]) async throws { + guard let driver = DatabaseManager.shared.activeDriver else { + throw DatabaseError.notConnected + } + + let dbType = connection.type + var allStatements: [String] = [] + + // Add BEGIN + allStatements.append("BEGIN") + + // Add user statements + allStatements.append(contentsOf: statements) + + // Add COMMIT + allStatements.append("COMMIT") + + // Execute all statements sequentially + do { + for sql in allStatements { + _ = try await driver.execute(query: sql) + } + } catch { + // Try to rollback on error + _ = try? await driver.execute(query: "ROLLBACK") + throw error + } + } + + // MARK: - Table Creation + /// Creates a new table from the provided options /// - Parameter options: Table creation configuration func createTable(_ options: TableCreationOptions) { diff --git a/TablePro/Views/MainContentView.swift b/TablePro/Views/MainContentView.swift index 15a36f624..3e449f10d 100644 --- a/TablePro/Views/MainContentView.swift +++ b/TablePro/Views/MainContentView.swift @@ -37,6 +37,7 @@ struct MainContentView: View { @State private var editingCell: CellPosition? @State private var notificationHandler: MainContentNotificationHandler? @State private var showDatabaseSwitcher = false + @StateObject private var sidebarEditState = MultiRowEditState() // MARK: - Environment @@ -124,6 +125,10 @@ struct MainContentView: View { } .onChange(of: selectedRowIndices) { _, newIndices in AppState.shared.hasRowSelection = !newIndices.isEmpty + updateSidebarEditState() + } + .onChange(of: currentTab?.resultRows) { _, _ in + updateSidebarEditState() } .onAppear { setupNotificationHandler() } .sheet(isPresented: $showDatabaseSwitcher) { @@ -209,7 +214,13 @@ struct MainContentView: View { RightSidebarView( tableName: currentTab?.tableName, tableMetadata: coordinator.tableMetadata, - selectedRowData: selectedRowDataForSidebar + selectedRowData: selectedRowDataForSidebar, + isEditable: isSidebarEditable, + isRowDeleted: isSelectedRowDeleted, + onSave: { + handleSidebarSave() + }, + editState: sidebarEditState ) .frame(width: 280) .task(id: currentTab?.tableName) { @@ -422,6 +433,170 @@ struct MainContentView: View { case .error: return .error("") } } + + // MARK: - Sidebar Edit Handling + + private func updateSidebarEditState() { + guard isSidebarEditable, + let tab = coordinator.tabManager.selectedTab, + !selectedRowIndices.isEmpty else { + sidebarEditState.fields = [] + return + } + + // Gather rows for selected indices + var allRows: [[String?]] = [] + for index in selectedRowIndices.sorted() { + if index < tab.resultRows.count { + allRows.append(tab.resultRows[index].values) + } + } + + // Get column types + let columnTypes = tab.columnTypes.map { $0.displayName } + + // Configure edit state (this will preserve pending edits if data hasn't changed) + sidebarEditState.configure( + selectedRowIndices: selectedRowIndices, + allRows: allRows, + columns: tab.resultColumns, + columnTypes: columnTypes + ) + } + + private func handleSidebarSave() { + Task { + await saveSidebarEdits() + } + } + + @MainActor + private func saveSidebarEdits() async { + guard let tab = coordinator.tabManager.selectedTab, + !selectedRowIndices.isEmpty, + let tableName = tab.tableName else { + return + } + + let editedFields = sidebarEditState.getEditedFields() + guard !editedFields.isEmpty else { return } + + do { + // Generate SQL for each selected row + var statements: [String] = [] + + for rowIndex in selectedRowIndices.sorted() { + guard rowIndex < tab.resultRows.count else { continue } + let row = tab.resultRows[rowIndex] + + // Build UPDATE statement + guard let updateSQL = generateUpdateSQL( + tableName: tableName, + rowIndex: rowIndex, + originalRow: row.values, + editedFields: editedFields, + columns: tab.resultColumns, + primaryKeyColumn: changeManager.primaryKeyColumn + ) else { + continue + } + + statements.append(updateSQL) + } + + guard !statements.isEmpty else { return } + + // Execute statements + try await coordinator.executeSidebarChanges(statements: statements) + + // Refresh query to show updated data + // The onChange(resultRows) handler will automatically update the sidebar + coordinator.runQuery() + + } catch { + // Show error using macOS alert + let alert = NSAlert() + alert.messageText = "Failed to Save Changes" + alert.informativeText = error.localizedDescription + alert.alertStyle = .warning + alert.addButton(withTitle: "OK") + alert.runModal() + } + } + + private func generateUpdateSQL( + tableName: String, + rowIndex: Int, + originalRow: [String?], + editedFields: [(columnIndex: Int, columnName: String, newValue: String?)], + columns: [String], + primaryKeyColumn: String? + ) -> String? { + guard let pkColumn = primaryKeyColumn, + let pkIndex = columns.firstIndex(of: pkColumn), + pkIndex < originalRow.count, + let pkValue = originalRow[pkIndex] else { + return nil + } + + let dbType = coordinator.connection.type + + // Build SET clause + let setClauses = editedFields.map { field -> String in + let quotedColumn = dbType.quoteIdentifier(field.columnName) + let value: String + if field.newValue == "__DEFAULT__" { + value = "DEFAULT" + } else if let newValue = field.newValue { + // Check if it's a SQL function (don't quote) + if isSQLFunction(newValue) { + value = newValue.trimmingCharacters(in: .whitespaces).uppercased() + } else { + value = "'\(SQLEscaping.escapeStringLiteral(newValue))'" + } + } else { + value = "NULL" + } + return "\(quotedColumn) = \(value)" + }.joined(separator: ", ") + + // Build WHERE clause + let quotedPK = dbType.quoteIdentifier(pkColumn) + let quotedPKValue = "'\(SQLEscaping.escapeStringLiteral(pkValue))'" + let whereClause = "\(quotedPK) = \(quotedPKValue)" + + // Add LIMIT clause for MySQL/MariaDB + let limitClause = (dbType == .mysql || dbType == .mariadb) ? " LIMIT 1" : "" + + return "UPDATE \(dbType.quoteIdentifier(tableName)) SET \(setClauses) WHERE \(whereClause)\(limitClause)" + } + + private func isSQLFunction(_ value: String) -> Bool { + let trimmed = value.trimmingCharacters(in: .whitespaces).uppercased() + + let sqlFunctions = [ + "NOW()", + "CURRENT_TIMESTAMP()", + "CURRENT_TIMESTAMP", + "CURDATE()", + "CURTIME()", + "UTC_TIMESTAMP()", + "UTC_DATE()", + "UTC_TIME()", + "LOCALTIME()", + "LOCALTIME", + "LOCALTIMESTAMP()", + "LOCALTIMESTAMP", + "SYSDATE()", + "UNIX_TIMESTAMP()", + "CURRENT_DATE()", + "CURRENT_DATE", + "CURRENT_TIME()", + "CURRENT_TIME", + ] + + return sqlFunctions.contains(trimmed) + } } // MARK: - Preview diff --git a/TablePro/Views/RightSidebar/EditableFieldView.swift b/TablePro/Views/RightSidebar/EditableFieldView.swift new file mode 100644 index 000000000..8d87e10e5 --- /dev/null +++ b/TablePro/Views/RightSidebar/EditableFieldView.swift @@ -0,0 +1,180 @@ +// +// EditableFieldView.swift +// TablePro +// +// Reusable editable field component for right sidebar. +// Native macOS form-style field with menu button. +// + +import SwiftUI + +/// Editable field view with native macOS styling +struct EditableFieldView: View { + let columnName: String + let columnType: String + let originalValue: String? + let hasMultipleValues: Bool + let isPendingNull: Bool + let isPendingDefault: Bool + + @Binding var value: String + + let onSetNull: () -> Void + let onSetDefault: () -> Void + let onSetFunction: (String) -> Void + + @FocusState private var isFocused: Bool + + private var displayValue: String { + if isPendingNull { + return "NULL" + } else if isPendingDefault { + return "DEFAULT" + } else { + return value + } + } + + private var placeholderText: String { + if hasMultipleValues { + return "Multiple values" + } else if isPendingNull { + return "NULL" + } else if isPendingDefault { + return "DEFAULT" + } else if let original = originalValue { + return original + } else { + return "" + } + } + + var body: some View { + VStack(alignment: .leading, spacing: 4) { + // Label + HStack(spacing: 4) { + Text(columnName) + .font(.system(size: DesignConstants.FontSize.small)) + .foregroundStyle(.secondary) + + Text(columnType) + .font(.system(size: DesignConstants.FontSize.tiny)) + .foregroundStyle(.tertiary) + } + + // Input row + HStack(spacing: 0) { + // Text field - custom styled for full control + TextField(placeholderText, text: $value) + .textFieldStyle(.plain) + .font(.system(size: DesignConstants.FontSize.small)) + .disabled(isPendingNull || isPendingDefault) + .focused($isFocused) + .padding(.horizontal, 6) + .padding(.vertical, 4) + .background(Color(NSColor.textBackgroundColor)) + .cornerRadius(5) + .overlay( + RoundedRectangle(cornerRadius: 5) + .strokeBorder(Color(NSColor.separatorColor), lineWidth: 0.5) + ) + + // Menu button - custom button to avoid any default indicators + Menu { + Button("Set NULL") { + onSetNull() + } + + Button("Set DEFAULT") { + onSetDefault() + } + + Divider() + + Menu("SQL Functions") { + Button("NOW()") { + onSetFunction("NOW()") + } + Button("CURRENT_TIMESTAMP()") { + onSetFunction("CURRENT_TIMESTAMP()") + } + Button("CURDATE()") { + onSetFunction("CURDATE()") + } + Button("CURTIME()") { + onSetFunction("CURTIME()") + } + Button("UTC_TIMESTAMP()") { + onSetFunction("UTC_TIMESTAMP()") + } + } + + if isPendingNull || isPendingDefault { + Divider() + Button("Clear") { + value = originalValue ?? "" + } + } + } label: { + Image(systemName: "ellipsis.circle") + .imageScale(.small) + .foregroundStyle(.secondary) + .frame(width: 20, height: 20) + } + .menuStyle(.borderlessButton) + .menuIndicator(.hidden) + .fixedSize() + .padding(.leading, 6) + .help("Set special value") + } + } + .padding(.vertical, 6) + } +} + +/// Read-only field view (for readonly mode or deleted rows) +struct ReadOnlyFieldView: View { + let columnName: String + let columnType: String + let value: String? + + var body: some View { + VStack(alignment: .leading, spacing: 4) { + // Label + HStack(spacing: 4) { + Text(columnName) + .font(.system(size: DesignConstants.FontSize.small)) + .foregroundStyle(.secondary) + + Text(columnType) + .font(.system(size: DesignConstants.FontSize.tiny)) + .foregroundStyle(.tertiary) + } + + // Value display - looks like disabled text field + HStack { + if let value = value { + Text(value) + .font(.system(size: DesignConstants.FontSize.small)) + .foregroundStyle(.primary) + .textSelection(.enabled) + .frame(maxWidth: .infinity, alignment: .leading) + } else { + Text("NULL") + .font(.system(size: DesignConstants.FontSize.small)) + .foregroundStyle(.tertiary) + .italic() + } + } + .padding(.horizontal, 6) + .padding(.vertical, 4) + .background(Color(NSColor.controlBackgroundColor)) + .cornerRadius(5) + .overlay( + RoundedRectangle(cornerRadius: 5) + .strokeBorder(Color(NSColor.separatorColor), lineWidth: 0.5) + ) + } + .padding(.vertical, 6) + } +} diff --git a/TablePro/Views/RightSidebar/RightSidebarView.swift b/TablePro/Views/RightSidebar/RightSidebarView.swift index a0c14ea2a..982f8296c 100644 --- a/TablePro/Views/RightSidebar/RightSidebarView.swift +++ b/TablePro/Views/RightSidebar/RightSidebarView.swift @@ -12,11 +12,20 @@ struct RightSidebarView: View { let tableName: String? let tableMetadata: TableMetadata? let selectedRowData: [(column: String, value: String?, type: String)]? + let isEditable: Bool + let isRowDeleted: Bool + let onSave: () -> Void + + @ObservedObject var editState: MultiRowEditState @State private var searchText: String = "" + @State private var fieldValues: [String] = [] private var mode: String { - selectedRowData != nil ? "Row Details" : "Table Info" + if selectedRowData != nil { + return isEditable ? "Edit Row" : "Row Details" + } + return "Table Info" } var body: some View { @@ -162,17 +171,70 @@ struct RightSidebarView: View { @ViewBuilder private func rowDetailContent(_ rowData: [(column: String, value: String?, type: String)]) -> some View { - let filtered = searchText.isEmpty ? rowData : rowData.filter { - $0.column.localizedCaseInsensitiveContains(searchText) || - ($0.value?.localizedCaseInsensitiveContains(searchText) ?? false) + let filtered = searchText.isEmpty ? editState.fields : editState.fields.filter { + $0.columnName.localizedCaseInsensitiveContains(searchText) || + ($0.originalValue?.localizedCaseInsensitiveContains(searchText) ?? false) } sectionHeader("FIELDS (\(filtered.count))") - ForEach(filtered, id: \.column) { field in - fieldRow(field) + ForEach(Array(filtered.enumerated()), id: \.element.columnName) { index, field in + if isEditable && !isRowDeleted { + editableFieldRow(field, at: index) + } else { + readonlyFieldRow(field) + } + } + + if isEditable && !isRowDeleted && editState.hasEdits { + saveButton + } + } + + private var saveButton: some View { + VStack(spacing: 0) { + Divider() + + Button(action: onSave) { + Text("Save Changes") + .frame(maxWidth: .infinity) + } + .buttonStyle(.borderedProminent) + .controlSize(.large) + .keyboardShortcut("s", modifiers: .command) + .padding(12) } } + + @ViewBuilder + private func editableFieldRow(_ field: FieldEditState, at index: Int) -> some View { + EditableFieldView( + columnName: field.columnName, + columnType: field.columnType, + originalValue: field.originalValue, + hasMultipleValues: field.hasMultipleValues, + isPendingNull: field.isPendingNull, + isPendingDefault: field.isPendingDefault, + value: Binding( + get: { field.pendingValue ?? field.originalValue ?? "" }, + set: { editState.updateField(at: index, value: $0) } + ), + onSetNull: { editState.setFieldToNull(at: index) }, + onSetDefault: { editState.setFieldToDefault(at: index) }, + onSetFunction: { editState.setFieldToFunction(at: index, function: $0) } + ) + .padding(.horizontal, 12) + } + + @ViewBuilder + private func readonlyFieldRow(_ field: FieldEditState) -> some View { + ReadOnlyFieldView( + columnName: field.columnName, + columnType: field.columnType, + value: field.originalValue + ) + .padding(.horizontal, 12) + } // MARK: - UI Components @@ -200,50 +262,13 @@ struct RightSidebarView: View { .padding(.vertical, 4) } - private func fieldRow(_ field: (column: String, value: String?, type: String)) -> some View { - VStack(alignment: .leading, spacing: 4) { - // Field name + type badge - HStack(spacing: 6) { - Text(field.column) - .font(.system(size: DesignConstants.FontSize.small, weight: .medium)) - .foregroundStyle(.primary) - - Text(field.type) - .font(.system(size: DesignConstants.FontSize.tiny)) - .foregroundStyle(.tertiary) - .padding(.horizontal, 4) - .padding(.vertical, DesignConstants.Spacing.xxxs) - .background(Color.secondary.opacity(0.1)) - .cornerRadius(3) - } - - // Value - if let value = field.value { - Text(value) - .font(.system(size: DesignConstants.FontSize.small)) - .foregroundStyle(.secondary) - .textSelection(.enabled) - .lineLimit(3) - } else { - Text("NULL") - .font(.system(size: DesignConstants.FontSize.caption, weight: .medium)) - .foregroundStyle(.secondary) - .padding(.horizontal, DesignConstants.Spacing.xxs) - .padding(.vertical, DesignConstants.Spacing.xxxs) - .background(Color.secondary.opacity(0.12)) - .cornerRadius(3) - } - } - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.horizontal, 12) - .padding(.vertical, DesignConstants.Spacing.xs) - } } // MARK: - Preview #Preview { - RightSidebarView( + @StateObject var editState = MultiRowEditState() + return RightSidebarView( tableName: "users", tableMetadata: TableMetadata( tableName: "users", @@ -258,7 +283,11 @@ struct RightSidebarView: View { createTime: Date(), updateTime: nil ), - selectedRowData: nil + selectedRowData: nil, + isEditable: false, + isRowDeleted: false, + onSave: {}, + editState: editState ) .frame(width: 280, height: 400) } From 219e7e80da071e3c6837913e632ef05ab333e267 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ng=C3=B4=20Qu=E1=BB=91c=20=C4=90=E1=BA=A1t?= Date: Thu, 15 Jan 2026 17:41:06 +0700 Subject: [PATCH 2/5] Update TablePro/Views/RightSidebar/RightSidebarView.swift MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Ngô Quốc Đạt --- TablePro/Views/RightSidebar/RightSidebarView.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/TablePro/Views/RightSidebar/RightSidebarView.swift b/TablePro/Views/RightSidebar/RightSidebarView.swift index 982f8296c..e52f1264f 100644 --- a/TablePro/Views/RightSidebar/RightSidebarView.swift +++ b/TablePro/Views/RightSidebar/RightSidebarView.swift @@ -19,7 +19,6 @@ struct RightSidebarView: View { @ObservedObject var editState: MultiRowEditState @State private var searchText: String = "" - @State private var fieldValues: [String] = [] private var mode: String { if selectedRowData != nil { From 7e9e44ca4671c6ca45d268816ac716bc1e6c1913 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ng=C3=B4=20Qu=E1=BB=91c=20=C4=90=E1=BA=A1t?= Date: Thu, 15 Jan 2026 17:41:20 +0700 Subject: [PATCH 3/5] Update TablePro/Views/RightSidebar/RightSidebarView.swift MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Ngô Quốc Đạt --- TablePro/Views/RightSidebar/RightSidebarView.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/TablePro/Views/RightSidebar/RightSidebarView.swift b/TablePro/Views/RightSidebar/RightSidebarView.swift index e52f1264f..137ff2046 100644 --- a/TablePro/Views/RightSidebar/RightSidebarView.swift +++ b/TablePro/Views/RightSidebar/RightSidebarView.swift @@ -177,9 +177,9 @@ struct RightSidebarView: View { sectionHeader("FIELDS (\(filtered.count))") - ForEach(Array(filtered.enumerated()), id: \.element.columnName) { index, field in + ForEach(filtered, id: \.columnName) { field in if isEditable && !isRowDeleted { - editableFieldRow(field, at: index) + editableFieldRow(field, at: field.columnIndex) } else { readonlyFieldRow(field) } From f9980a9d3ce1a95529dc4b834a554a0fe908c4ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ng=C3=B4=20Qu=E1=BB=91c=20=C4=90=E1=BA=A1t?= Date: Thu, 15 Jan 2026 17:41:31 +0700 Subject: [PATCH 4/5] Update TablePro/Views/Main/MainContentCoordinator.swift MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Ngô Quốc Đạt --- TablePro/Views/Main/MainContentCoordinator.swift | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/TablePro/Views/Main/MainContentCoordinator.swift b/TablePro/Views/Main/MainContentCoordinator.swift index bf2fa8b6e..074ea3438 100644 --- a/TablePro/Views/Main/MainContentCoordinator.swift +++ b/TablePro/Views/Main/MainContentCoordinator.swift @@ -1143,8 +1143,15 @@ final class MainContentCoordinator: ObservableObject { let dbType = connection.type var allStatements: [String] = [] - // Add BEGIN - allStatements.append("BEGIN") + // Add database-specific BEGIN / START TRANSACTION + let beginStatement: String + switch dbType { + case .mysql, .mariadb: + beginStatement = "START TRANSACTION" + default: + beginStatement = "BEGIN" + } + allStatements.append(beginStatement) // Add user statements allStatements.append(contentsOf: statements) From 8529282bcd3c3b4a0c8b013a29519ff725085172 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ng=C3=B4=20Qu=E1=BB=91c=20=C4=90=E1=BA=A1t?= Date: Thu, 15 Jan 2026 17:41:40 +0700 Subject: [PATCH 5/5] Update TablePro/Views/MainContentView.swift MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Ngô Quốc Đạt --- TablePro/Views/MainContentView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TablePro/Views/MainContentView.swift b/TablePro/Views/MainContentView.swift index 3e449f10d..1f72f67ff 100644 --- a/TablePro/Views/MainContentView.swift +++ b/TablePro/Views/MainContentView.swift @@ -550,7 +550,7 @@ struct MainContentView: View { } else if let newValue = field.newValue { // Check if it's a SQL function (don't quote) if isSQLFunction(newValue) { - value = newValue.trimmingCharacters(in: .whitespaces).uppercased() + value = newValue.trimmingCharacters(in: .whitespaces) } else { value = "'\(SQLEscaping.escapeStringLiteral(newValue))'" }