Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 177 additions & 0 deletions TablePro/Models/MultiRowEditState.swift
Original file line number Diff line number Diff line change
@@ -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<Int> = []
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<Int>,
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..<fields.count {
fields[i].pendingValue = nil
fields[i].isPendingNull = false
fields[i].isPendingDefault = false
}
}

/// Get all edited fields with their new values
func getEditedFields() -> [(columnIndex: Int, columnName: String, newValue: String?)] {
fields.compactMap { field in
guard field.hasEdit else { return nil }
return (field.columnIndex, field.columnName, field.effectiveValue)
}
}
}
20 changes: 19 additions & 1 deletion TablePro/Views/Main/Extensions/MainContentView+Bindings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
39 changes: 39 additions & 0 deletions TablePro/Views/Main/MainContentCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1134,6 +1134,45 @@ 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 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)

// 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) {
Expand Down
Loading