diff --git a/CHANGELOG.md b/CHANGELOG.md index 437406649..c17f723cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - AI Chat views: replace custom pill buttons with native `.borderless` styles, switch hardcoded text colors to semantic system colors, use relative font sizing in Markdown rendering, align spacing to the 8-pt grid, and add accessibility labels to icon-only buttons - Translucent backgrounds (Welcome sidebar, settings banners, ER diagram toolbar, JSON editor controls, Pro feature scrim) honor the system Reduce Transparency and Increase Contrast accessibility settings, swapping the material for a solid surface color when either is on - Internal: result-grid sortable header drops the custom resize cursor handling that duplicated AppKit's built-in column-edge resize, and consolidates three sort delegate methods into one that carries the full sort state. No user-facing change; multi-column sort, shift-click cycle, and the column resize cursor still work the same. +- Internal: Redis sidebar key tree uses SwiftUI `OutlineGroup` instead of recursive `DisclosureGroup` + `ForEach` wrapped in `AnyView`. Expansion state is now managed natively per branch identifier; the explicit `expandedPrefixes` set is gone. ### Fixed diff --git a/TablePro/Models/UI/RedisKeyNode.swift b/TablePro/Models/UI/RedisKeyNode.swift index 607858df7..2888869c5 100644 --- a/TablePro/Models/UI/RedisKeyNode.swift +++ b/TablePro/Models/UI/RedisKeyNode.swift @@ -23,6 +23,13 @@ internal enum RedisKeyNode: Identifiable, Hashable { } } + var children: [RedisKeyNode]? { + switch self { + case .namespace(_, _, let children, _): return children + case .key: return nil + } + } + // Hash on id only (children excluded for performance) func hash(into hasher: inout Hasher) { hasher.combine(id) diff --git a/TablePro/ViewModels/RedisKeyTreeViewModel.swift b/TablePro/ViewModels/RedisKeyTreeViewModel.swift index a10e1aec4..82b421e2c 100644 --- a/TablePro/ViewModels/RedisKeyTreeViewModel.swift +++ b/TablePro/ViewModels/RedisKeyTreeViewModel.swift @@ -13,7 +13,6 @@ internal final class RedisKeyTreeViewModel { private static let maxKeys = 50_000 var rootNodes: [RedisKeyNode] = [] - var expandedPrefixes: Set = [] var isLoading = false var isTruncated = false var separator: String = ":" @@ -65,7 +64,6 @@ internal final class RedisKeyTreeViewModel { func clear() { rootNodes = [] allKeys = [] - expandedPrefixes = [] isTruncated = false } diff --git a/TablePro/Views/Sidebar/RedisKeyTreeView.swift b/TablePro/Views/Sidebar/RedisKeyTreeView.swift index af9e9dd1f..ae7487813 100644 --- a/TablePro/Views/Sidebar/RedisKeyTreeView.swift +++ b/TablePro/Views/Sidebar/RedisKeyTreeView.swift @@ -7,7 +7,6 @@ import SwiftUI internal struct RedisKeyTreeView: View { let nodes: [RedisKeyNode] - @Binding var expandedPrefixes: Set let isLoading: Bool let isTruncated: Bool var onSelectNamespace: ((String) -> Void)? @@ -29,7 +28,9 @@ internal struct RedisKeyTreeView: View { .font(.caption) .padding(.vertical, 4) } else { - renderNodes(nodes) + OutlineGroup(nodes, children: \.children) { node in + row(for: node) + } if isTruncated { Text("Showing first 50,000 keys") .foregroundStyle(.secondary) @@ -39,65 +40,41 @@ internal struct RedisKeyTreeView: View { } } - private func renderNodes(_ items: [RedisKeyNode]) -> AnyView { - AnyView( - ForEach(items) { node in - switch node { - case .namespace(let name, let fullPrefix, let children, let keyCount): - DisclosureGroup(isExpanded: Binding( - get: { expandedPrefixes.contains(fullPrefix) }, - set: { expanded in - if expanded { - expandedPrefixes.insert(fullPrefix) - } else { - expandedPrefixes.remove(fullPrefix) - } - } - )) { - renderNodes(children) - } label: { - namespaceLabel(name: name, keyCount: keyCount, fullPrefix: fullPrefix) - } - case .key(let name, let fullKey, let keyType): - keyLabel(name: name, fullKey: fullKey, keyType: keyType) + @ViewBuilder + private func row(for node: RedisKeyNode) -> some View { + switch node { + case .namespace(let name, let fullPrefix, _, let keyCount): + Button { + onSelectNamespace?(fullPrefix) + } label: { + HStack { + Label(name, systemImage: "folder") + .foregroundStyle(.primary) + Spacer() + Text("\(keyCount)") + .font(.caption2) + .foregroundStyle(.secondary) + .padding(.horizontal, 6) + .padding(.vertical, 1) + .background(.quaternary, in: Capsule()) } } - ) - } - - private func namespaceLabel(name: String, keyCount: Int, fullPrefix: String) -> some View { - Button { - onSelectNamespace?(fullPrefix) - } label: { - HStack { - Label(name, systemImage: "folder") - .foregroundStyle(.primary) - Spacer() - Text("\(keyCount)") - .font(.caption2) - .foregroundStyle(.secondary) - .padding(.horizontal, 6) - .padding(.vertical, 1) - .background(.quaternary, in: Capsule()) - } - } - .buttonStyle(.plain) - } - - private func keyLabel(name: String, fullKey: String, keyType: String) -> some View { - Button { - onSelectKey?(fullKey, keyType) - } label: { - HStack { - Label(name, systemImage: keyTypeIcon(keyType)) - .foregroundStyle(.primary) - Spacer() - Text(keyType) - .font(.caption2) - .foregroundStyle(.tertiary) + .buttonStyle(.plain) + case .key(let name, let fullKey, let keyType): + Button { + onSelectKey?(fullKey, keyType) + } label: { + HStack { + Label(name, systemImage: keyTypeIcon(keyType)) + .foregroundStyle(.primary) + Spacer() + Text(keyType) + .font(.caption2) + .foregroundStyle(.tertiary) + } } + .buttonStyle(.plain) } - .buttonStyle(.plain) } private func keyTypeIcon(_ type: String) -> String { diff --git a/TablePro/Views/Sidebar/SidebarView.swift b/TablePro/Views/Sidebar/SidebarView.swift index c1c3a6f28..e103ca24b 100644 --- a/TablePro/Views/Sidebar/SidebarView.swift +++ b/TablePro/Views/Sidebar/SidebarView.swift @@ -222,10 +222,6 @@ struct SidebarView: View { Section(isExpanded: $viewModel.isRedisKeysExpanded) { RedisKeyTreeView( nodes: keyTreeVM.displayNodes(searchText: viewModel.searchText), - expandedPrefixes: Binding( - get: { keyTreeVM.expandedPrefixes }, - set: { keyTreeVM.expandedPrefixes = $0 } - ), isLoading: keyTreeVM.isLoading, isTruncated: keyTreeVM.isTruncated, onSelectNamespace: { prefix in