Skip to content

Commit d004e3c

Browse files
committed
fix: inline tree rendering with AnyView for reliable List selection across folders
1 parent 86830f8 commit d004e3c

3 files changed

Lines changed: 547 additions & 101 deletions

File tree

TablePro/Views/Sidebar/FavoritesTabView.swift

Lines changed: 79 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ struct FavoritesTabView: View {
1313
@State private var selectedFavoriteIds: Set<String> = []
1414
@State private var folderToDelete: SQLFavoriteFolder?
1515
@State private var showDeleteFolderAlert = false
16+
@FocusState private var isRenameFocused: Bool
1617
let searchText: String
1718
private weak var coordinator: MainContentCoordinator?
1819

@@ -69,32 +70,92 @@ struct FavoritesTabView: View {
6970

7071
private func favoritesList(_ items: [FavoriteTreeItem]) -> some View {
7172
List(selection: $selectedFavoriteIds) {
72-
ForEach(items) { item in
73-
FavoriteTreeItemRow(
74-
item: item,
75-
viewModel: viewModel,
76-
coordinator: coordinator,
77-
onDeleteFolder: { folder in
78-
folderToDelete = folder
79-
showDeleteFolderAlert = true
80-
}
81-
)
82-
.tag(item.id)
83-
}
73+
flattenedRows(items)
8474
}
8575
.listStyle(.sidebar)
8676
.scrollContentBackground(.hidden)
8777
.onDeleteCommand {
8878
deleteSelectedFavorites()
8979
}
90-
.contextMenu {
91-
if !selectedFavoriteIds.isEmpty {
92-
Button(role: .destructive) {
93-
deleteSelectedFavorites()
94-
} label: {
95-
Label(String(localized: "Delete Selected"), systemImage: "trash")
80+
}
81+
82+
/// Renders tree items with DisclosureGroup for folders.
83+
/// Each favorite row gets `.tag()` so List selection works across all nesting levels.
84+
private func flattenedRows(_ items: [FavoriteTreeItem]) -> AnyView {
85+
AnyView(
86+
ForEach(items) { item in
87+
switch item {
88+
case .favorite(let favorite):
89+
FavoriteRowView(favorite: favorite)
90+
.tag("fav-\(favorite.id)")
91+
.overlay {
92+
DoubleClickDetector {
93+
coordinator?.insertFavorite(favorite)
94+
}
95+
}
96+
.contextMenu {
97+
FavoriteItemContextMenu(
98+
favorite: favorite,
99+
viewModel: viewModel,
100+
coordinator: coordinator
101+
)
102+
}
103+
case .folder(let folder, let children):
104+
DisclosureGroup(isExpanded: Binding(
105+
get: { viewModel.expandedFolderIds.contains(folder.id) },
106+
set: { expanded in
107+
if expanded {
108+
viewModel.expandedFolderIds.insert(folder.id)
109+
} else {
110+
viewModel.expandedFolderIds.remove(folder.id)
111+
}
112+
}
113+
)) {
114+
flattenedRows(children)
115+
} label: {
116+
folderLabel(folder)
117+
}
96118
}
97119
}
120+
)
121+
}
122+
123+
@ViewBuilder
124+
private func folderLabel(_ folder: SQLFavoriteFolder) -> some View {
125+
if viewModel.renamingFolderId == folder.id {
126+
HStack(spacing: 4) {
127+
Image(systemName: "folder")
128+
TextField(
129+
"",
130+
text: Binding(
131+
get: { viewModel.renamingFolderName },
132+
set: { viewModel.renamingFolderName = $0 }
133+
)
134+
)
135+
.textFieldStyle(.roundedBorder)
136+
.focused($isRenameFocused)
137+
.onSubmit {
138+
viewModel.commitRenameFolder(folder)
139+
}
140+
.onExitCommand {
141+
viewModel.renamingFolderId = nil
142+
}
143+
.onAppear {
144+
isRenameFocused = true
145+
}
146+
}
147+
} else {
148+
Label(folder.name, systemImage: "folder")
149+
.contextMenu {
150+
FolderContextMenu(
151+
folder: folder,
152+
viewModel: viewModel,
153+
onDelete: { f in
154+
folderToDelete = f
155+
showDeleteFolderAlert = true
156+
}
157+
)
158+
}
98159
}
99160
}
100161

@@ -190,89 +251,6 @@ struct FavoritesTabView: View {
190251
}
191252
}
192253

193-
// MARK: - Recursive Tree Item View
194-
195-
struct FavoriteTreeItemRow: View {
196-
let item: FavoriteTreeItem
197-
let viewModel: FavoritesSidebarViewModel
198-
weak var coordinator: MainContentCoordinator?
199-
var onDeleteFolder: ((SQLFavoriteFolder) -> Void)?
200-
@FocusState private var isRenameFocused: Bool
201-
202-
var body: some View {
203-
switch item {
204-
case .favorite(let favorite):
205-
FavoriteRowView(favorite: favorite)
206-
.overlay {
207-
DoubleClickDetector {
208-
coordinator?.insertFavorite(favorite)
209-
}
210-
}
211-
.contextMenu {
212-
FavoriteItemContextMenu(
213-
favorite: favorite,
214-
viewModel: viewModel,
215-
coordinator: coordinator
216-
)
217-
}
218-
case .folder(let folder, let children):
219-
DisclosureGroup(isExpanded: Binding(
220-
get: { viewModel.expandedFolderIds.contains(folder.id) },
221-
set: { isExpanded in
222-
if isExpanded {
223-
viewModel.expandedFolderIds.insert(folder.id)
224-
} else {
225-
viewModel.expandedFolderIds.remove(folder.id)
226-
}
227-
}
228-
)) {
229-
ForEach(children) { child in
230-
FavoriteTreeItemRow(
231-
item: child,
232-
viewModel: viewModel,
233-
coordinator: coordinator,
234-
onDeleteFolder: onDeleteFolder
235-
)
236-
.tag(child.id)
237-
}
238-
} label: {
239-
if viewModel.renamingFolderId == folder.id {
240-
HStack(spacing: 4) {
241-
Image(systemName: "folder")
242-
TextField(
243-
"",
244-
text: Binding(
245-
get: { viewModel.renamingFolderName },
246-
set: { viewModel.renamingFolderName = $0 }
247-
)
248-
)
249-
.textFieldStyle(.roundedBorder)
250-
.focused($isRenameFocused)
251-
.onSubmit {
252-
viewModel.commitRenameFolder(folder)
253-
}
254-
.onExitCommand {
255-
viewModel.renamingFolderId = nil
256-
}
257-
.onAppear {
258-
isRenameFocused = true
259-
}
260-
}
261-
} else {
262-
Label(folder.name, systemImage: "folder")
263-
.contextMenu {
264-
FolderContextMenu(
265-
folder: folder,
266-
viewModel: viewModel,
267-
onDelete: onDeleteFolder ?? { _ in }
268-
)
269-
}
270-
}
271-
}
272-
}
273-
}
274-
}
275-
276254
// MARK: - Context Menus
277255

278256
private struct FavoriteItemContextMenu: View {

0 commit comments

Comments
 (0)