Skip to content

Commit dea9176

Browse files
committed
refactor: replace structure view notifications with direct coordinator calls
1 parent abdf28a commit dea9176

8 files changed

Lines changed: 86 additions & 97 deletions

TablePro/TableProApp.swift

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -454,25 +454,21 @@ extension Notification.Name {
454454
// Multi-listener broadcasts (Sidebar + Coordinator + StructureView)
455455
static let refreshData = Notification.Name("refreshData")
456456

457-
// Data operations (still posted by DataGrid / context menus / StructureView subscribers)
457+
// Data operations (still posted by DataGrid / context menus)
458458
static let deleteSelectedRows = Notification.Name("deleteSelectedRows")
459459
static let addNewRow = Notification.Name("addNewRow")
460460
static let duplicateRow = Notification.Name("duplicateRow")
461461
static let copySelectedRows = Notification.Name("copySelectedRows")
462462
static let pasteRows = Notification.Name("pasteRows")
463-
static let undoChange = Notification.Name("undoChange")
464-
static let redoChange = Notification.Name("redoChange")
465463

466464
// Tab operations
467465
static let newQueryTab = Notification.Name("newQueryTab")
468466

469467
// Sidebar operations (still posted by SidebarView / ConnectionStatusView)
470468
static let openDatabaseSwitcher = Notification.Name("openDatabaseSwitcher")
471469

472-
// Structure view operations (still posted by QueryEditorView)
470+
// Query editor operations
473471
static let explainQuery = Notification.Name("explainQuery")
474-
static let saveStructureChanges = Notification.Name("saveStructureChanges")
475-
static let previewStructureSQL = Notification.Name("previewStructureSQL")
476472

477473
// File opening notifications
478474
static let openSQLFiles = Notification.Name("openSQLFiles")

TablePro/Views/Main/Child/MainEditorContentView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ struct MainEditorContentView: View {
249249
private func resultsSection(tab: QueryTab) -> some View {
250250
VStack(spacing: 0) {
251251
if tab.showStructure, let tableName = tab.tableName {
252-
TableStructureView(tableName: tableName, connection: connection, toolbarState: coordinator.toolbarState)
252+
TableStructureView(tableName: tableName, connection: connection, toolbarState: coordinator.toolbarState, coordinator: coordinator)
253253
.id(tableName)
254254
.frame(maxHeight: .infinity)
255255
} else if let explainText = tab.explainText {

TablePro/Views/Main/Extensions/MainContentCoordinator+SQLPreview.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ extension MainContentCoordinator {
1717
tableOperationOptions: [String: TableOperationOptions]
1818
) {
1919
if tabManager.selectedTab?.showStructure == true {
20-
// Structure view handles its own preview via notification
21-
NotificationCenter.default.post(name: .previewStructureSQL, object: nil)
20+
// Structure view handles its own preview via direct call
21+
structureActions?.previewSQL?()
2222
} else {
2323
generatePreviewSQL(
2424
pendingTruncates: pendingTruncates,

TablePro/Views/Main/MainContentCommandActions.swift

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ final class MainContentCommandActions {
240240

241241
func copySelectedRows() {
242242
if coordinator?.tabManager.selectedTab?.showStructure == true {
243-
NotificationCenter.default.post(name: .copySelectedRows, object: nil)
243+
coordinator?.structureActions?.copyRows?()
244244
} else {
245245
let indices = selectedRowIndices.wrappedValue
246246
coordinator?.copySelectedRowsToClipboard(indices: indices)
@@ -254,7 +254,7 @@ final class MainContentCommandActions {
254254

255255
func pasteRows() {
256256
if coordinator?.tabManager.selectedTab?.showStructure == true {
257-
NotificationCenter.default.post(name: .pasteRows, object: nil)
257+
coordinator?.structureActions?.pasteRows?()
258258
} else {
259259
var indices = selectedRowIndices.wrappedValue
260260
var cell = editingCell.wrappedValue
@@ -339,9 +339,9 @@ final class MainContentCommandActions {
339339
return
340340
}
341341

342-
// Structure view saves are notification-based
342+
// Structure view saves via direct coordinator call
343343
if coordinator.tabManager.selectedTab?.showStructure == true {
344-
NotificationCenter.default.post(name: .saveStructureChanges, object: nil)
344+
coordinator.structureActions?.saveChanges?()
345345
performClose()
346346
return
347347
}
@@ -408,8 +408,7 @@ final class MainContentCommandActions {
408408
func saveChanges() {
409409
// Check if we're in structure view mode
410410
if coordinator?.tabManager.selectedTab?.showStructure == true {
411-
// Post notification for structure view to handle
412-
NotificationCenter.default.post(name: .saveStructureChanges, object: nil)
411+
coordinator?.structureActions?.saveChanges?()
413412
} else if rightPanelState.editState.hasEdits {
414413
// Save sidebar edits if the right panel has pending changes
415414
rightPanelState.onSave?()
@@ -473,7 +472,7 @@ final class MainContentCommandActions {
473472

474473
func undoChange() {
475474
if coordinator?.tabManager.selectedTab?.showStructure == true {
476-
NotificationCenter.default.post(name: .undoChange, object: nil)
475+
coordinator?.structureActions?.undo?()
477476
} else {
478477
var indices = selectedRowIndices.wrappedValue
479478
coordinator?.undoLastChange(selectedRowIndices: &indices)
@@ -483,7 +482,7 @@ final class MainContentCommandActions {
483482

484483
func redoChange() {
485484
if coordinator?.tabManager.selectedTab?.showStructure == true {
486-
NotificationCenter.default.post(name: .redoChange, object: nil)
485+
coordinator?.structureActions?.redo?()
487486
} else {
488487
coordinator?.redoLastChange()
489488
}

TablePro/Views/Main/MainContentCoordinator.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ final class MainContentCoordinator {
6464
/// Direct reference to sidebar viewmodel — eliminates global notification broadcasts
6565
weak var sidebarViewModel: SidebarViewModel?
6666

67+
/// Direct reference to structure view actions — eliminates notification broadcasts
68+
weak var structureActions: StructureViewActionHandler?
69+
6770
// MARK: - Published State
6871

6972
var schemaProvider: SQLSchemaProvider
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
//
2+
// StructureViewActionHandler.swift
3+
// TablePro
4+
//
5+
// Action handler for structure view — allows coordinator to call
6+
// structure-view actions directly instead of broadcasting notifications.
7+
//
8+
9+
import Foundation
10+
11+
/// Provides direct action dispatch from coordinator to structure view,
12+
/// replacing notification-based communication.
13+
@MainActor
14+
final class StructureViewActionHandler {
15+
var saveChanges: (() -> Void)?
16+
var previewSQL: (() -> Void)?
17+
var copyRows: (() -> Void)?
18+
var pasteRows: (() -> Void)?
19+
var undo: (() -> Void)?
20+
var redo: (() -> Void)?
21+
}

TablePro/Views/Structure/TableStructureView.swift

Lines changed: 20 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ struct TableStructureView: View {
1818
let tableName: String
1919
let connection: DatabaseConnection
2020
let toolbarState: ConnectionToolbarState
21+
let coordinator: MainContentCoordinator?
2122

2223
@State private var selectedTab: StructureTab = .columns
2324
@State private var columns: [ColumnInfo] = []
@@ -41,11 +42,13 @@ struct TableStructureView: View {
4142
@State private var sortState = SortState()
4243
@State private var editingCell: CellPosition?
4344
@State private var structureColumnLayout = ColumnLayoutState()
45+
@State private var actionHandler = StructureViewActionHandler()
4446

45-
init(tableName: String, connection: DatabaseConnection, toolbarState: ConnectionToolbarState) {
47+
init(tableName: String, connection: DatabaseConnection, toolbarState: ConnectionToolbarState, coordinator: MainContentCoordinator?) {
4648
self.tableName = tableName
4749
self.connection = connection
4850
self.toolbarState = toolbarState
51+
self.coordinator = coordinator
4952

5053
// Initialize wrappedChangeManager using the StateObject's wrappedValue
5154
let manager = StructureChangeManager()
@@ -71,51 +74,30 @@ struct TableStructureView: View {
7174
AppState.shared.isCurrentTabEditable = (selectedTab != .ddl)
7275
AppState.shared.hasRowSelection = !selectedRows.isEmpty
7376
AppState.shared.hasStructureChanges = structureChangeManager.hasChanges
77+
78+
// Wire action handler for direct coordinator calls
79+
actionHandler.saveChanges = {
80+
if self.structureChangeManager.hasChanges && self.selectedTab != .ddl {
81+
Task { await self.executeSchemaChanges() }
82+
}
83+
}
84+
actionHandler.previewSQL = { self.generateStructurePreviewSQL() }
85+
actionHandler.copyRows = { self.handleCopyRows(self.selectedRows) }
86+
actionHandler.pasteRows = { self.handlePaste() }
87+
actionHandler.undo = { self.handleUndo() }
88+
actionHandler.redo = { self.handleRedo() }
89+
coordinator?.structureActions = actionHandler
7490
}
7591
.onDisappear {
7692
AppState.shared.isCurrentTabEditable = false
7793
AppState.shared.hasRowSelection = false
7894
AppState.shared.hasStructureChanges = false
95+
coordinator?.structureActions = nil
7996
}
8097
.onChange(of: structureChangeManager.hasChanges) { _, newValue in
8198
AppState.shared.hasStructureChanges = newValue
8299
}
83100
.onReceive(NotificationCenter.default.publisher(for: .refreshData), perform: onRefreshData)
84-
.onReceive(
85-
Publishers.Merge(
86-
NotificationCenter.default.publisher(for: .saveStructureChanges),
87-
NotificationCenter.default.publisher(for: .previewStructureSQL)
88-
)
89-
.debounce(for: .milliseconds(50), scheduler: RunLoop.main)
90-
) { notification in
91-
if notification.name == .saveStructureChanges {
92-
if structureChangeManager.hasChanges && selectedTab != .ddl {
93-
Task {
94-
await executeSchemaChanges()
95-
}
96-
}
97-
} else {
98-
generateStructurePreviewSQL()
99-
}
100-
}
101-
.onReceive(NotificationCenter.default.publisher(for: .copySelectedRows)) { _ in
102-
handleCopyRows(selectedRows)
103-
}
104-
.onReceive(NotificationCenter.default.publisher(for: .pasteRows)) { _ in
105-
handlePaste()
106-
}
107-
.onReceive(
108-
Publishers.Merge(
109-
NotificationCenter.default.publisher(for: .undoChange),
110-
NotificationCenter.default.publisher(for: .redoChange)
111-
)
112-
) { notification in
113-
if notification.name == .undoChange {
114-
handleUndo()
115-
} else {
116-
handleRedo()
117-
}
118-
}
119101
}
120102

121103
// MARK: - Toolbar
@@ -895,7 +877,8 @@ struct TableStructureView: View {
895877
username: "root",
896878
type: .mysql
897879
),
898-
toolbarState: ConnectionToolbarState()
880+
toolbarState: ConnectionToolbarState(),
881+
coordinator: nil
899882
)
900883
.frame(width: 800, height: 600)
901884
}

docs/development/notification-refactor.md

Lines changed: 30 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -106,63 +106,50 @@ Gave the coordinator a direct `weak var sidebarViewModel` reference, replacing 1
106106

107107
---
108108

109-
## Phase 4: Replace Sidebar Action Notifications with `@FocusedValue`
109+
## Phase 4: Replace Sidebar Action Notifications with Direct Calls
110110

111-
**Status:** Not started
112-
113-
Menu items post global notifications to reach the sidebar. These should use `@FocusedValue` to call the active window's sidebar directly.
114-
115-
- [ ] `copyTableNames` — menu → `SidebarViewModel.copySelectedTableNames()`
116-
- [ ] `truncateTables` — menu → `SidebarViewModel.batchToggleTruncate()`
117-
- [ ] `clearSelection` — menu → `SidebarViewModel.selectedTables.removeAll()`
118-
- [ ] `showAllTables` — menu → coordinator action
119-
- [ ] `showTableStructure` — sidebar context menu → coordinator
120-
- [ ] `editViewDefinition` — sidebar context menu → coordinator
121-
- [ ] `createView` — sidebar context menu → coordinator
122-
- [ ] `exportTables` — sidebar context menu → coordinator
123-
- [ ] `importTables` — sidebar context menu → coordinator
111+
**Status:** Done (PR [#286](https://github.com/datlechin/TablePro/pull/286))
124112

125-
### Pattern:
113+
Replaced 9 sidebar action notifications with direct coordinator calls and `@FocusedValue` routing. Context menu actions call coordinator directly. Menu bar uses `actions?.copyTableNames()` and `actions?.truncateTables()` via `@FocusedValue`.
126114

127-
```swift
128-
// Define focused value
129-
struct SidebarActionsKey: FocusedValueKey {
130-
typealias Value = SidebarViewModel
131-
}
115+
- [x] `copyTableNames` — menu → `actions?.copyTableNames()``coordinator.sidebarViewModel.copySelectedTableNames()`
116+
- [x] `truncateTables` — menu → `actions?.truncateTables()``coordinator.sidebarViewModel.batchToggleTruncate()`
117+
- [x] `clearSelection` — dead (no sender), removed both subscribers
118+
- [x] `showAllTables``SidebarView` calls `coordinator?.showAllTablesMetadata()` directly
119+
- [x] `showTableStructure` — context menu → `coordinator?.openTableTab(_, showStructure:)`
120+
- [x] `editViewDefinition` — context menu → `coordinator?.editViewDefinition(_:)`
121+
- [x] `createView` — context menu → `coordinator?.createView()`
122+
- [x] `exportTables` — context menu → `coordinator?.openExportDialog()`
123+
- [x] `importTables` — context menu → `coordinator?.openImportDialog()`
132124

133-
extension FocusedValues {
134-
var sidebarActions: SidebarViewModel? { ... }
135-
}
136-
137-
// In SidebarView
138-
.focusedValue(\.sidebarActions, viewModel)
139-
140-
// In menu
141-
Button("Copy Table Names") {
142-
focusedSidebarActions?.copySelectedTableNames()
143-
}
144-
```
125+
Also extracted `createView()`, `editViewDefinition(_:)`, `openExportDialog()`, `openImportDialog()` from `MainContentCommandActions` into `MainContentCoordinator+SidebarActions.swift`. Removed all notification infrastructure from `SidebarViewModel` (`import Combine`, `cancellables`, `setupNotifications()`).
145126

146127
---
147128

148129
## Phase 5: Replace Structure View Notifications with Coordinator Pattern
149130

150-
**Status:** Not started
131+
**Status:** Done
151132

152-
`MainContentCommandActions` routes commands to `TableStructureView` via notifications because the structure view is deeply embedded and not directly accessible.
133+
Created `StructureViewActionHandler` class with closure properties for each action. `TableStructureView` wires closures in `.onAppear` and registers handler with coordinator. Senders call `coordinator.structureActions?.method?()` instead of posting notifications.
153134

154-
### Notifications to replace:
135+
### Notifications replaced:
155136

156-
- [ ] `copySelectedRows` (structure path)
157-
- [ ] `pasteRows` (structure path)
158-
- [ ] `undoChange` (structure path)
159-
- [ ] `redoChange` (structure path)
160-
- [ ] `saveStructureChanges`
161-
- [ ] `previewStructureSQL`
137+
- [x] `copySelectedRows` (structure path) — now `structureActions?.copyRows?()`
138+
- [x] `pasteRows` (structure path) — now `structureActions?.pasteRows?()`
139+
- [x] `undoChange` — removed notification name, now `structureActions?.undo?()`
140+
- [x] `redoChange` — removed notification name, now `structureActions?.redo?()`
141+
- [x] `saveStructureChanges` — removed notification name, now `structureActions?.saveChanges?()`
142+
- [x] `previewStructureSQL` — removed notification name, now `structureActions?.previewSQL?()`
162143

163-
### Strategy:
144+
### Files changed:
164145

165-
Create a `StructureViewActions` protocol/class that `TableStructureView` registers with the coordinator. The coordinator calls methods directly instead of broadcasting.
146+
- **New:** `Views/Structure/StructureViewActionHandler.swift` — action handler class
147+
- **Modified:** `MainContentCoordinator.swift` — added `weak var structureActions`
148+
- **Modified:** `TableStructureView.swift` — wire closures on appear, removed 6 `.onReceive` handlers
149+
- **Modified:** `MainEditorContentView.swift` — pass coordinator to `TableStructureView`
150+
- **Modified:** `MainContentCommandActions.swift` — 6 notification posts → direct calls
151+
- **Modified:** `MainContentCoordinator+SQLPreview.swift` — notification post → direct call
152+
- **Modified:** `TableProApp.swift` — removed 4 notification name definitions
166153

167154
---
168155

0 commit comments

Comments
 (0)