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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- AnyChangeManager uses ChangeManaging protocol instead of closure-based type erasure, removing all runtime `[Any]` downcasts
- Row selection state moved from MainContentView @State to GridSelectionState @Observable class, preventing full view tree invalidation on every row click
- QueryTab decomposed into focused sub-types: TabExecutionState, TabTableContext, TabQueryContent, TabDisplayState
- Inspector: dense field list, PK/FK key icons, Set NULL/DEFAULT in picker dropdowns, toolbar tracking separator
Expand Down
135 changes: 41 additions & 94 deletions TablePro/Core/ChangeTracking/AnyChangeManager.swift
Original file line number Diff line number Diff line change
@@ -1,102 +1,38 @@
//
// AnyChangeManager.swift
// TablePro
//
// Type-erased wrapper for change managers (data and structure).
// Allows DataGridView to work with both DataChangeManager and StructureChangeManager.
//

import Foundation
import Observation

/// Type-erased change manager wrapper
@MainActor
protocol ChangeManaging: AnyObject {
var hasChanges: Bool { get }
var reloadVersion: Int { get }
var canRedo: Bool { get }
var rowChanges: [RowChange] { get }
func isRowDeleted(_ rowIndex: Int) -> Bool
func recordCellChange(
rowIndex: Int,
columnIndex: Int,
columnName: String,
oldValue: String?,
newValue: String?,
originalRow: [String?]?
)
func undoRowDeletion(rowIndex: Int)
func undoRowInsertion(rowIndex: Int)
func consumeChangedRowIndices() -> Set<Int>
}

@Observable
@MainActor
final class AnyChangeManager {
@ObservationIgnored private var dataManager: DataChangeManager?
@ObservationIgnored private var structureManager: StructureChangeManager?

var hasChanges: Bool {
dataManager?.hasChanges ?? structureManager?.hasChanges ?? false
}

var reloadVersion: Int {
dataManager?.reloadVersion ?? structureManager?.reloadVersion ?? 0
}

@ObservationIgnored private let _isRowDeleted: (Int) -> Bool
@ObservationIgnored private let _getChanges: () -> [Any]
@ObservationIgnored private let _canRedo: () -> Bool
@ObservationIgnored private let _recordCellChange: ((Int, Int, String, String?, String?, [String?]) -> Void)?
@ObservationIgnored private let _undoRowDeletion: ((Int) -> Void)?
@ObservationIgnored private let _undoRowInsertion: ((Int) -> Void)?
@ObservationIgnored private let _consumeChangedRowIndices: (() -> Set<Int>)?

// MARK: - Initializers

/// Wrap a DataChangeManager
init(dataManager: DataChangeManager) {
self.dataManager = dataManager
self._isRowDeleted = { rowIndex in
dataManager.isRowDeleted(rowIndex)
}
self._getChanges = {
dataManager.changes
}
self._canRedo = {
dataManager.canRedo
}
self._recordCellChange = { rowIndex, columnIndex, columnName, oldValue, newValue, originalRow in
dataManager.recordCellChange(
rowIndex: rowIndex,
columnIndex: columnIndex,
columnName: columnName,
oldValue: oldValue,
newValue: newValue,
originalRow: originalRow
)
}
self._undoRowDeletion = { rowIndex in
dataManager.undoRowDeletion(rowIndex: rowIndex)
}
self._undoRowInsertion = { rowIndex in
dataManager.undoRowInsertion(rowIndex: rowIndex)
}
self._consumeChangedRowIndices = {
dataManager.consumeChangedRowIndices()
}
}

/// Wrap a StructureChangeManager
init(structureManager: StructureChangeManager) {
self.structureManager = structureManager
self._isRowDeleted = { _ in false } // Structure doesn't track row deletions
self._getChanges = {
Array(structureManager.pendingChanges.values)
}
self._canRedo = {
structureManager.canRedo
}
self._recordCellChange = nil // Structure uses custom editing logic
self._undoRowDeletion = nil
self._undoRowInsertion = nil
self._consumeChangedRowIndices = {
structureManager.consumeChangedRowIndices()
}
}
@ObservationIgnored private let wrapped: any ChangeManaging

// MARK: - Public API

var canRedo: Bool {
_canRedo()
}
var hasChanges: Bool { wrapped.hasChanges }
var reloadVersion: Int { wrapped.reloadVersion }
var canRedo: Bool { wrapped.canRedo }
var rowChanges: [RowChange] { wrapped.rowChanges }

func isRowDeleted(_ rowIndex: Int) -> Bool {
_isRowDeleted(rowIndex)
}

var changes: [Any] {
_getChanges()
wrapped.isRowDeleted(rowIndex)
}

func recordCellChange(
Expand All @@ -107,18 +43,29 @@ final class AnyChangeManager {
newValue: String?,
originalRow: [String?]
) {
_recordCellChange?(rowIndex, columnIndex, columnName, oldValue, newValue, originalRow)
wrapped.recordCellChange(
rowIndex: rowIndex,
columnIndex: columnIndex,
columnName: columnName,
oldValue: oldValue,
newValue: newValue,
originalRow: originalRow
)
}

func undoRowDeletion(rowIndex: Int) {
_undoRowDeletion?(rowIndex)
wrapped.undoRowDeletion(rowIndex: rowIndex)
}

func undoRowInsertion(rowIndex: Int) {
_undoRowInsertion?(rowIndex)
wrapped.undoRowInsertion(rowIndex: rowIndex)
}

func consumeChangedRowIndices() -> Set<Int> {
_consumeChangedRowIndices?() ?? []
wrapped.consumeChangedRowIndices()
}

init(_ manager: any ChangeManaging) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Update test-facing AnyChangeManager API changes

This refactor removes the labeled initializers in favor of init(_:) (and also renames the exposed change list to rowChanges), but the existing test target still calls AnyChangeManager(dataManager:), AnyChangeManager(structureManager:), and wrapper.changes in TableProTests/Core/ChangeTracking/AnyChangeManagerTests.swift. In environments that build tests (CI/local test runs), this change set will fail to compile until those call sites are updated or compatibility shims are kept.

Useful? React with 👍 / 👎.

self.wrapped = manager
}
}
19 changes: 11 additions & 8 deletions TablePro/Core/ChangeTracking/DataChangeManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@ struct UndoResult {
/// @MainActor ensures thread-safe access - critical for avoiding EXC_BAD_ACCESS
/// when multiple queries complete simultaneously (e.g., rapid sorting over SSH tunnel)
@MainActor @Observable
final class DataChangeManager {
final class DataChangeManager: ChangeManaging {
private static let logger = Logger(subsystem: "com.TablePro", category: "DataChangeManager")
var changes: [RowChange] = []
var rowChanges: [RowChange] { changes }
var hasChanges: Bool = false
var reloadVersion: Int = 0

Expand Down Expand Up @@ -234,7 +235,7 @@ final class DataChangeManager {
undoManager.registerUndo(withTarget: self) { target in
target.applyDataUndo(.cellEdit(
rowIndex: rowIndex, columnIndex: columnIndex, columnName: columnName,
previousValue: oldValue, newValue: newValue
previousValue: oldValue, newValue: newValue, originalRow: nil
))
}
undoManager.setActionName(String(localized: "Edit Cell"))
Expand Down Expand Up @@ -289,7 +290,7 @@ final class DataChangeManager {
undoManager.registerUndo(withTarget: self) { target in
target.applyDataUndo(.cellEdit(
rowIndex: rowIndex, columnIndex: columnIndex, columnName: columnName,
previousValue: oldValue, newValue: newValue
previousValue: oldValue, newValue: newValue, originalRow: originalRow
))
}
undoManager.setActionName(String(localized: "Edit Cell"))
Expand Down Expand Up @@ -504,11 +505,11 @@ final class DataChangeManager {
// swiftlint:disable:next function_body_length
private func applyDataUndo(_ action: UndoAction) {
switch action {
case .cellEdit(let rowIndex, let columnIndex, let columnName, let previousValue, let newValue):
case .cellEdit(let rowIndex, let columnIndex, let columnName, let previousValue, let newValue, let originalRow):
undoManager.registerUndo(withTarget: self) { target in
target.applyDataUndo(.cellEdit(
rowIndex: rowIndex, columnIndex: columnIndex, columnName: columnName,
previousValue: newValue, newValue: previousValue
previousValue: newValue, newValue: previousValue, originalRow: originalRow
))
}
undoManager.setActionName(String(localized: "Edit Cell"))
Expand Down Expand Up @@ -558,7 +559,7 @@ final class DataChangeManager {
} else {
recordCellChangeForRedo(
rowIndex: rowIndex, columnIndex: columnIndex, columnName: columnName,
oldValue: newValue, newValue: previousValue
oldValue: newValue, newValue: previousValue, originalRow: originalRow
)
}
changedRowIndices.insert(rowIndex)
Expand Down Expand Up @@ -708,7 +709,8 @@ final class DataChangeManager {
columnIndex: Int,
columnName: String,
oldValue: String?,
newValue: String?
newValue: String?,
originalRow: [String?]?
) {
let cellChange = CellChange(
rowIndex: rowIndex,
Expand Down Expand Up @@ -758,7 +760,8 @@ final class DataChangeManager {
}
} else {
let rowChange = RowChange(
rowIndex: rowIndex, type: .update, cellChanges: [cellChange]
rowIndex: rowIndex, type: .update, cellChanges: [cellChange],
originalRow: originalRow
)
changes.append(rowChange)
changeIndex[updateKey] = changes.count - 1
Expand Down
3 changes: 2 additions & 1 deletion TablePro/Core/ChangeTracking/DataChangeModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ enum UndoAction {
columnIndex: Int,
columnName: String,
previousValue: String?,
newValue: String?
newValue: String?,
originalRow: [String?]?
)
case rowInsertion(rowIndex: Int)
case rowDeletion(rowIndex: Int, originalRow: [String?])
Expand Down
21 changes: 20 additions & 1 deletion TablePro/Core/SchemaTracking/StructureChangeManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import Observation

/// Manager for tracking and applying schema changes
@MainActor @Observable
final class StructureChangeManager {
final class StructureChangeManager: ChangeManaging {
private(set) var pendingChanges: [SchemaChangeIdentifier: SchemaChange] = [:]
@ObservationIgnored private var changeOrder: [SchemaChangeIdentifier] = []
private(set) var validationErrors: [SchemaChangeIdentifier: String] = [:]
Expand Down Expand Up @@ -882,6 +882,25 @@ final class StructureChangeManager {
let tab: StructureTab
let row: Int
}

// MARK: - ChangeManaging Conformance (Data-Specific No-Ops)

var rowChanges: [RowChange] { [] }

func isRowDeleted(_ rowIndex: Int) -> Bool { false }

func recordCellChange(
rowIndex: Int,
columnIndex: Int,
columnName: String,
oldValue: String?,
newValue: String?,
originalRow: [String?]?
) {}

func undoRowDeletion(rowIndex: Int) {}

func undoRowInsertion(rowIndex: Int) {}
}

// MARK: - Schema Undo Action
Expand Down
2 changes: 1 addition & 1 deletion TablePro/Core/Services/Query/RowOperationsManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ final class RowOperationsManager {

private func applyUndoResult(_ result: UndoResult, resultRows: inout [[String?]]) -> Set<Int>? {
switch result.action {
case .cellEdit(let rowIndex, let columnIndex, _, let previousValue, _):
case .cellEdit(let rowIndex, let columnIndex, _, let previousValue, _, _):
if rowIndex < resultRows.count {
resultRows[rowIndex][columnIndex] = previousValue
}
Expand Down
4 changes: 2 additions & 2 deletions TablePro/Views/Main/Child/MainEditorContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ struct MainEditorContentView: View {
}
// Fallback before onAppear initializes cachedChangeManager.
// Safe: onAppear fires before any user interaction needs it.
return AnyChangeManager(dataManager: changeManager)
return AnyChangeManager(changeManager)
}

// MARK: - Body
Expand Down Expand Up @@ -152,7 +152,7 @@ struct MainEditorContentView: View {
}
.onAppear {
updateHasQueryText()
cachedChangeManager = AnyChangeManager(dataManager: changeManager)
cachedChangeManager = AnyChangeManager(changeManager)
if let tab = tabManager.selectedTab {
cacheRowProvider(for: tab)
}
Expand Down
3 changes: 1 addition & 2 deletions TablePro/Views/Results/DataGridCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -312,8 +312,7 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData
return
}

for change in changeManager.changes {
guard let rowChange = change as? RowChange else { continue }
for rowChange in changeManager.rowChanges {
let rowIndex = rowChange.rowIndex
let isDeleted = rowChange.type == .delete
let isInserted = rowChange.type == .insert
Expand Down
5 changes: 2 additions & 3 deletions TablePro/Views/Results/DataGridView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -281,8 +281,7 @@ struct DataGridView: NSViewRepresentable {
// Re-apply pending cell edits only when changes have been modified
if changeManager.reloadVersion != coordinator.lastReapplyVersion {
coordinator.lastReapplyVersion = changeManager.reloadVersion
for change in changeManager.changes {
guard let rowChange = change as? RowChange else { continue }
for rowChange in changeManager.rowChanges {
for cellChange in rowChange.cellChanges {
coordinator.rowProvider.updateValue(
cellChange.newValue,
Expand Down Expand Up @@ -730,7 +729,7 @@ struct DataGridView: NSViewRepresentable {
],
columns: ["id", "name", "email"]
),
changeManager: AnyChangeManager(dataManager: DataChangeManager()),
changeManager: AnyChangeManager(DataChangeManager()),
isEditable: true,
selectedRowIndices: .constant([]),
sortState: .constant(SortState()),
Expand Down
2 changes: 1 addition & 1 deletion TablePro/Views/Structure/CreateTableView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ struct CreateTableView: View {

let manager = StructureChangeManager()
_structureChangeManager = State(wrappedValue: manager)
_wrappedChangeManager = State(wrappedValue: AnyChangeManager(structureManager: manager))
_wrappedChangeManager = State(wrappedValue: AnyChangeManager(manager))
_gridDelegate = State(wrappedValue: CreateTableGridDelegate(
structureChangeManager: manager,
structureTab: .columns,
Expand Down
2 changes: 1 addition & 1 deletion TablePro/Views/Structure/TableStructureView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ struct TableStructureView: View {

let manager = StructureChangeManager()
_structureChangeManager = State(wrappedValue: manager)
_wrappedChangeManager = State(wrappedValue: AnyChangeManager(structureManager: manager))
_wrappedChangeManager = State(wrappedValue: AnyChangeManager(manager))
_gridDelegate = State(wrappedValue: StructureGridDelegate(
structureChangeManager: manager,
selectedTab: .columns,
Expand Down
Loading