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 @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Sidebar object icons can be turned off for a plain list of names. Right-click anywhere in the sidebar and use View Options, or toggle Show Object Icons from the View menu or Settings > General. Tables staged for truncate or delete keep their marker.
- Redshift external schemas now list their tables. Spectrum, federated query, cross-database, and datashare schemas showed up empty because their tables are not in the standard catalog.
- External schemas are marked in the sidebar, and their tables show an external icon. External tables open read-only, because Redshift rejects `UPDATE` and `DELETE` on them.

Expand Down
11 changes: 9 additions & 2 deletions TablePro/Models/Settings/GeneralSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,18 @@ struct GeneralSettings: Codable, Equatable {
/// Whether to show database object comments in the sidebar and data grid headers
var showObjectComments: Bool

/// Whether sidebar rows show a type icon before the object name
var showObjectIcons: Bool

static let `default` = GeneralSettings(
startupBehavior: .reopenLast,
language: .system,
automaticallyCheckForUpdates: true,
queryTimeoutSeconds: 60,
shareAnalytics: true,
showRecentTables: false,
showObjectComments: true
showObjectComments: true,
showObjectIcons: true
)

init(
Expand All @@ -86,7 +90,8 @@ struct GeneralSettings: Codable, Equatable {
queryTimeoutSeconds: Int = 60,
shareAnalytics: Bool = true,
showRecentTables: Bool = false,
showObjectComments: Bool = true
showObjectComments: Bool = true,
showObjectIcons: Bool = true
) {
self.startupBehavior = startupBehavior
self.language = language
Expand All @@ -95,6 +100,7 @@ struct GeneralSettings: Codable, Equatable {
self.shareAnalytics = shareAnalytics
self.showRecentTables = showRecentTables
self.showObjectComments = showObjectComments
self.showObjectIcons = showObjectIcons
}

init(from decoder: Decoder) throws {
Expand All @@ -106,5 +112,6 @@ struct GeneralSettings: Codable, Equatable {
shareAnalytics = try container.decodeIfPresent(Bool.self, forKey: .shareAnalytics) ?? true
showRecentTables = try container.decodeIfPresent(Bool.self, forKey: .showRecentTables) ?? false
showObjectComments = try container.decodeIfPresent(Bool.self, forKey: .showObjectComments) ?? true
showObjectIcons = try container.decodeIfPresent(Bool.self, forKey: .showObjectIcons) ?? true
}
}
9 changes: 9 additions & 0 deletions TablePro/TableProApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,13 @@ struct AppMenuCommands: Commands {
)
}

private var showObjectIconsBinding: Binding<Bool> {
Binding(
get: { settingsManager.general.showObjectIcons },
set: { settingsManager.general.showObjectIcons = $0 }
)
}

private func shortcut(for action: ShortcutAction) -> KeyboardShortcut? {
settingsManager.keyboard.keyboardShortcut(for: action)
}
Expand Down Expand Up @@ -717,6 +724,8 @@ struct AppMenuCommands: Commands {
.pickerStyle(.inline)
.disabled(!(actions?.canSwitchSidebarLayout ?? false))

Toggle(String(localized: "Show Object Icons"), isOn: showObjectIconsBinding)

Toggle(String(localized: "Show Object Comments"), isOn: showObjectCommentsBinding)

Divider()
Expand Down
3 changes: 3 additions & 0 deletions TablePro/Views/Settings/GeneralSettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ struct GeneralSettingsView: View {
Toggle("Show recent tables", isOn: $settings.showRecentTables)
.help("Adds a Recent section at the top of the Tables sidebar with the last tables you opened per connection and database.")

Toggle("Show object icons", isOn: $settings.showObjectIcons)
.help("Shows a type icon before each object name in the sidebar. Turn it off for a plain list of names.")

Toggle("Show object comments", isOn: $settings.showObjectComments)
.help("Shows database object comments next to tables in the sidebar and in grid column headers.")

Expand Down
7 changes: 6 additions & 1 deletion TablePro/Views/Sidebar/DatabaseTreeRowView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,11 @@ struct DatabaseTreeRowView: View {

var body: some View {
if hasContextMenu {
row.contextMenu { menuItems }
row.contextMenu {
menuItems
Divider()
SidebarViewOptionsMenu()
}
} else {
row
}
Expand Down Expand Up @@ -127,6 +131,7 @@ struct DatabaseTreeRowView: View {
} icon: {
Image(systemName: systemImage)
}
.sidebarRowIcon(visible: AppSettingsManager.shared.general.showObjectIcons)
.lineLimit(1)
.foregroundStyle(foreground(isActive: isActive, isSystem: isSystem))
}
Expand Down
3 changes: 3 additions & 0 deletions TablePro/Views/Sidebar/FavoritesTabView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,9 @@ internal struct FavoritesTabView: View {
.contextMenu(forSelectionType: FavoriteSelection.self) { selection in
if let selected = selection.first {
contextMenu(for: selected)
Divider()
}
SidebarViewOptionsMenu()
} primaryAction: { selection in
guard let selected = selection.first else { return }
handlePrimaryAction(selected)
Expand All @@ -292,6 +294,7 @@ internal struct FavoritesTabView: View {
Image(systemName: TableRowLogic.iconName(for: table.type))
.sidebarTint(Color.accentColor)
}
.sidebarRowIcon(visible: AppSettingsManager.shared.general.showObjectIcons)
.tag(FavoriteSelection.table(database: activeDatabase, schema: table.schema, name: table.name))
.accessibilityLabel(
TableRowLogic.accessibilityLabel(table: table, isPendingDelete: false, isPendingTruncate: false)
Expand Down
1 change: 1 addition & 0 deletions TablePro/Views/Sidebar/RoutineRowView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ struct RoutineRowView: View {
.sidebarTint(Color.accentColor)
.frame(width: 16)
}
.sidebarRowIcon(visible: AppSettingsManager.shared.general.showObjectIcons)
.accessibilityElement(children: .combine)
.accessibilityLabel(RoutineRowLogic.accessibilityLabel(for: routine))
.help(RoutineRowLogic.tooltip(for: routine) ?? routine.name)
Expand Down
24 changes: 24 additions & 0 deletions TablePro/Views/Sidebar/SidebarRowIcon.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//
// SidebarRowIcon.swift
// TablePro
//

import SwiftUI

private struct SidebarRowIcon: ViewModifier {
let isVisible: Bool

func body(content: Content) -> some View {
if isVisible {
content.labelStyle(.titleAndIcon)
} else {
content.labelStyle(.titleOnly)
}
}
}

extension View {
func sidebarRowIcon(visible: Bool) -> some View {
modifier(SidebarRowIcon(isVisible: visible))
}
}
6 changes: 6 additions & 0 deletions TablePro/Views/Sidebar/SidebarTreeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ struct SidebarTreeView: View {
.tag(table)
.contextMenu {
tableContextMenu(table)
Divider()
SidebarViewOptionsMenu()
}
}

Expand Down Expand Up @@ -174,6 +176,8 @@ struct SidebarTreeView: View {
Button(String(localized: "Clear Recent Tables")) {
sidebarState.clearRecentTables(inDatabase: activeDatabase)
}
Divider()
SidebarViewOptionsMenu()
}
}
} header: {
Expand All @@ -195,6 +199,8 @@ struct SidebarTreeView: View {
Button(String(localized: "Refresh")) {
reloadTables(for: schema)
}
Divider()
SidebarViewOptionsMenu()
}
}

Expand Down
8 changes: 8 additions & 0 deletions TablePro/Views/Sidebar/SidebarView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,8 @@ struct SidebarView: View {
Button(String(localized: "Clear Recent Tables")) {
sidebarState.clearRecentTables(inDatabase: activeDatabase)
}
Divider()
SidebarViewOptionsMenu()
}
}
} header: {
Expand Down Expand Up @@ -452,6 +454,8 @@ struct SidebarView: View {
onBatchToggleDelete: { viewModel.batchToggleDelete(tableNames: $0) },
coordinator: coordinator
)
Divider()
SidebarViewOptionsMenu()
} primaryAction: { selection in
guard let table = selection.first else { return }
onDoubleClick?(table)
Expand Down Expand Up @@ -502,6 +506,8 @@ struct SidebarView: View {
RoutineContextMenu(routine: routine) { selected in
coordinator?.showRoutineDDL(selected)
}
Divider()
SidebarViewOptionsMenu()
}
}
} else {
Expand All @@ -528,6 +534,8 @@ struct SidebarView: View {
.help(helpLabel)
.contextMenu {
sectionHeaderMenu(for: kind, title: title)
Divider()
SidebarViewOptionsMenu()
}
}

Expand Down
17 changes: 17 additions & 0 deletions TablePro/Views/Sidebar/SidebarViewOptionsMenu.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//
// SidebarViewOptionsMenu.swift
// TablePro
//

import SwiftUI

struct SidebarViewOptionsMenu: View {
@State private var settingsManager = AppSettingsManager.shared

var body: some View {
Menu(String(localized: "View Options")) {
Toggle("Icons", isOn: $settingsManager.general.showObjectIcons)
Toggle("Comments", isOn: $settingsManager.general.showObjectComments)
}
}
}
34 changes: 28 additions & 6 deletions TablePro/Views/Sidebar/TableRowView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ enum TableRowLogic {
}
}

static func showsLeadingIcon(showObjectIcons: Bool, isPendingTruncate: Bool, isPendingDelete: Bool) -> Bool {
showObjectIcons || isPendingTruncate || isPendingDelete
}

static func accessibilityLabel(table: TableInfo, isPendingDelete: Bool, isPendingTruncate: Bool, isFavorite: Bool = false) -> String {
let kind = accessibilityKindLabel(for: table.type)
var label = String(format: String(localized: "%@: %@"), kind, table.name)
Expand Down Expand Up @@ -60,6 +64,18 @@ struct TableRow: View {
return comment
}

private var showsObjectIcon: Bool {
AppSettingsManager.shared.general.showObjectIcons
}

private var showsLeadingIcon: Bool {
TableRowLogic.showsLeadingIcon(
showObjectIcons: showsObjectIcon,
isPendingTruncate: isPendingTruncate,
isPendingDelete: isPendingDelete
)
}

@ViewBuilder
private var pendingStateBadge: some View {
if isPendingDelete {
Expand Down Expand Up @@ -90,13 +106,19 @@ struct TableRow: View {
}
}
} icon: {
Image(systemName: TableRowLogic.iconName(for: table.type))
.sidebarTint(Color.accentColor)
.frame(width: 16)
.overlay(alignment: .bottomTrailing) {
pendingStateBadge
}
if showsObjectIcon {
Image(systemName: TableRowLogic.iconName(for: table.type))
.sidebarTint(Color.accentColor)
.frame(width: 16)
.overlay(alignment: .bottomTrailing) {
pendingStateBadge
}
} else {
pendingStateBadge
.frame(width: 16)
}
}
.sidebarRowIcon(visible: showsLeadingIcon)

Spacer(minLength: 4)

Expand Down
35 changes: 35 additions & 0 deletions TableProTests/Models/GeneralSettingsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,38 @@ struct GeneralSettingsTests {
#expect(decoded.showRecentTables == true)
}
}

@Suite("GeneralSettings.showObjectIcons")
struct GeneralSettingsObjectIconsTests {
@Test("Defaults to on")
func defaultsOn() {
#expect(GeneralSettings.default.showObjectIcons == true)
#expect(GeneralSettings().showObjectIcons == true)
}

@Test("Decoding settings saved before the key existed keeps icons on")
func decodesMissingKeyAsOn() throws {
let json = Data(#"{"startupBehavior":"showWelcome"}"#.utf8)
let decoded = try JSONDecoder().decode(GeneralSettings.self, from: json)
#expect(decoded.showObjectIcons == true)
}

@Test("Round-trips when disabled")
func roundTripsDisabled() throws {
var settings = GeneralSettings()
settings.showObjectIcons = false
let data = try JSONEncoder().encode(settings)
let decoded = try JSONDecoder().decode(GeneralSettings.self, from: data)
#expect(decoded.showObjectIcons == false)
}

@Test("Icons and comments are independent")
func independentFromComments() throws {
var settings = GeneralSettings()
settings.showObjectIcons = false
let data = try JSONEncoder().encode(settings)
let decoded = try JSONDecoder().decode(GeneralSettings.self, from: data)
#expect(decoded.showObjectIcons == false)
#expect(decoded.showObjectComments == true)
}
}
29 changes: 29 additions & 0 deletions TableProTests/Views/TableRowLogicTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,33 @@ struct TableRowLogicTests {
func externalTableIconIsDistinct() {
#expect(TableRowLogic.iconName(for: .externalTable) != TableRowLogic.iconName(for: .table))
}

// MARK: - Leading Icon Visibility

@Test("Leading icon shows when object icons are enabled")
func leadingIconShownWhenEnabled() {
#expect(TableRowLogic.showsLeadingIcon(showObjectIcons: true, isPendingTruncate: false, isPendingDelete: false))
}

@Test("Leading icon hides when object icons are disabled")
func leadingIconHiddenWhenDisabled() {
#expect(!TableRowLogic.showsLeadingIcon(showObjectIcons: false, isPendingTruncate: false, isPendingDelete: false))
}

@Test("Pending delete keeps the leading slot when object icons are disabled")
func leadingIconSurvivesPendingDelete() {
#expect(TableRowLogic.showsLeadingIcon(showObjectIcons: false, isPendingTruncate: false, isPendingDelete: true))
}

@Test("Pending truncate keeps the leading slot when object icons are disabled")
func leadingIconSurvivesPendingTruncate() {
#expect(TableRowLogic.showsLeadingIcon(showObjectIcons: false, isPendingTruncate: true, isPendingDelete: false))
}

@Test("Hiding icons leaves the accessibility label naming the object kind")
func accessibilityLabelIndependentOfIconVisibility() {
let view = TestFixtures.makeTableInfo(name: "active_users", type: .view)
#expect(TableRowLogic.accessibilityLabel(table: view, isPendingDelete: false, isPendingTruncate: false) == "View: active_users")
#expect(!TableRowLogic.showsLeadingIcon(showObjectIcons: false, isPendingTruncate: false, isPendingDelete: false))
}
}
1 change: 1 addition & 0 deletions docs/customization/settings.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ Reopened tabs restore their SQL, cursor position, sort, filters, page, and colum
| Setting | Default | Description |
|---------|---------|-------------|
| **Show recent tables** | Off | Adds a Recent section at the top of the sidebar with the last 10 tables opened per connection and database |
| **Show object icons** | On | Shows a type icon before each object name in the sidebar. Turn it off for a plain list of names. Also on the **View** menu, and under **View Options** in the sidebar right-click menu |
| **Show object comments** | On | Shows database object comments next to tables in the sidebar and in grid column headers |
| **Default layout for new connections** | List | List or Tree, for servers that support a database tree. Switch the current connection from the **View** menu |

Expand Down
Loading