feat(settings): move Outline version to sidebar footer, drop Installation section
Removed the Integrations & Installation category and its lone Installation section entirely — Outline's server version now shows in the settings sidebar footer instead, alongside Outpost's own version (installation.info, fetched once when the sidebar appears). About stays where it was, unaffected.
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import OutlineKit
|
||||||
|
|
||||||
/// Swapped into the real sidebar's content slot (search field, collections
|
/// Swapped into the real sidebar's content slot (search field, collections
|
||||||
/// tree, account footer) while Settings is open — same sidebar, different
|
/// tree, account footer) while Settings is open — same sidebar, different
|
||||||
@@ -8,13 +9,23 @@ import SwiftUI
|
|||||||
/// back.
|
/// back.
|
||||||
///
|
///
|
||||||
/// Grouped by `SettingsCategory` — `general` (ours) sits under an "Outpost"
|
/// Grouped by `SettingsCategory` — `general` (ours) sits under an "Outpost"
|
||||||
/// header at the top, then Outline's own Account/Workspace/Integrations &
|
/// header at the top, then Outline's own Account/Workspace groups, matching
|
||||||
/// Installation groups, matching the settings page structure of the
|
/// the settings page structure of the Outline web app. Outline's own server
|
||||||
/// Outline web app.
|
/// version has no dedicated section (there used to be an Integrations &
|
||||||
|
/// Installation category for just that) — it's cheap enough to show
|
||||||
|
/// unconditionally in the footer here instead, alongside Outpost's own
|
||||||
|
/// version.
|
||||||
struct SettingsSidebarList: View {
|
struct SettingsSidebarList: View {
|
||||||
@Binding var selection: SettingsSection?
|
@Binding var selection: SettingsSection?
|
||||||
let onDone: () -> Void
|
let onDone: () -> Void
|
||||||
|
|
||||||
|
@Environment(SessionStore.self) private var session
|
||||||
|
@State private var outlineVersion: String?
|
||||||
|
|
||||||
|
private var outpostVersion: String {
|
||||||
|
Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "0.0.1"
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
HStack {
|
HStack {
|
||||||
@@ -46,12 +57,36 @@ struct SettingsSidebarList: View {
|
|||||||
|
|
||||||
Divider()
|
Divider()
|
||||||
|
|
||||||
|
versionFooter
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
|
||||||
Button("Done", action: onDone)
|
Button("Done", action: onDone)
|
||||||
.keyboardShortcut(.cancelAction)
|
.keyboardShortcut(.cancelAction)
|
||||||
.buttonStyle(.borderedProminent)
|
.buttonStyle(.borderedProminent)
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
.padding(12)
|
.padding(12)
|
||||||
}
|
}
|
||||||
|
.task { await refreshOutlineVersion() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private var versionFooter: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
Text("Outpost \(outpostVersion)")
|
||||||
|
if let outlineVersion {
|
||||||
|
Text("Outline \(outlineVersion)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundStyle(.tertiary)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.padding(.horizontal, 16)
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func refreshOutlineVersion() async {
|
||||||
|
guard let apiClient = session.apiClient else { return }
|
||||||
|
outlineVersion = try? await apiClient.installationInfo().version
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -44,9 +44,6 @@ struct SettingsView: View {
|
|||||||
@State private var apiKeys: [OutlineAPIKey] = []
|
@State private var apiKeys: [OutlineAPIKey] = []
|
||||||
@State private var isLoadingApiKeys = false
|
@State private var isLoadingApiKeys = false
|
||||||
@State private var apiKeysErrorMessage: String?
|
@State private var apiKeysErrorMessage: String?
|
||||||
@State private var installationInfo: OutlineInstallationInfo?
|
|
||||||
@State private var isLoadingInstallationInfo = false
|
|
||||||
@State private var installationInfoErrorMessage: String?
|
|
||||||
|
|
||||||
/// Full Local Sync and cache-clearing both need a real connection to be
|
/// Full Local Sync and cache-clearing both need a real connection to be
|
||||||
/// safe — clearing while offline (or letting Full Local Sync think it
|
/// safe — clearing while offline (or letting Full Local Sync think it
|
||||||
@@ -90,7 +87,6 @@ struct SettingsView: View {
|
|||||||
case .notifications: notificationsDetail
|
case .notifications: notificationsDetail
|
||||||
case .passkeys: passkeysDetail
|
case .passkeys: passkeysDetail
|
||||||
case .apiAccess: apiAccessDetail
|
case .apiAccess: apiAccessDetail
|
||||||
case .installation: installationDetail
|
|
||||||
case .offlineSync: offlineSyncDetail
|
case .offlineSync: offlineSyncDetail
|
||||||
case .advanced: advancedDetail
|
case .advanced: advancedDetail
|
||||||
case .about: aboutDetail
|
case .about: aboutDetail
|
||||||
@@ -98,9 +94,8 @@ struct SettingsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Everything in Account/Workspace/Integrations & Installation that
|
/// Everything in Account/Workspace that isn't `.profile` — real content
|
||||||
/// isn't `.profile` — real content lands section by section; this is
|
/// lands section by section; this is just the nav skeleton until then.
|
||||||
/// just the nav skeleton until then.
|
|
||||||
private var comingSoonDetail: some View {
|
private var comingSoonDetail: some View {
|
||||||
VStack(spacing: 16) {
|
VStack(spacing: 16) {
|
||||||
sectionHeader
|
sectionHeader
|
||||||
@@ -824,55 +819,6 @@ struct SettingsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Installation
|
|
||||||
|
|
||||||
private var installationDetail: some View {
|
|
||||||
VStack(alignment: .leading, spacing: 16) {
|
|
||||||
sectionHeader
|
|
||||||
|
|
||||||
if isLoadingInstallationInfo && installationInfo == nil {
|
|
||||||
ProgressView().controlSize(.small)
|
|
||||||
} else if let installationInfoErrorMessage {
|
|
||||||
Text(installationInfoErrorMessage)
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.red)
|
|
||||||
} else if let installationInfo {
|
|
||||||
VStack(alignment: .leading, spacing: 6) {
|
|
||||||
labeledRow("Version", installationInfo.version)
|
|
||||||
Divider()
|
|
||||||
if installationInfo.versionsBehind > 0 {
|
|
||||||
labeledRow("Latest Version", installationInfo.latestVersion)
|
|
||||||
Divider()
|
|
||||||
Label(
|
|
||||||
"\(installationInfo.versionsBehind) version\(installationInfo.versionsBehind == 1 ? "" : "s") behind",
|
|
||||||
systemImage: "exclamationmark.triangle.fill"
|
|
||||||
)
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.orange)
|
|
||||||
} else {
|
|
||||||
Label("Up to date", systemImage: "checkmark.circle.fill")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.green)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.frame(maxWidth: 420, alignment: .leading)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.task { await refreshInstallationInfo() }
|
|
||||||
}
|
|
||||||
|
|
||||||
private func refreshInstallationInfo() async {
|
|
||||||
guard let apiClient = session.apiClient else { return }
|
|
||||||
isLoadingInstallationInfo = true
|
|
||||||
defer { isLoadingInstallationInfo = false }
|
|
||||||
do {
|
|
||||||
installationInfo = try await apiClient.installationInfo()
|
|
||||||
installationInfoErrorMessage = nil
|
|
||||||
} catch {
|
|
||||||
installationInfoErrorMessage = outlineErrorMessage(error, fallback: "Couldn't load installation info.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Offline & Sync
|
// MARK: - Offline & Sync
|
||||||
|
|
||||||
private var offlineSyncDetail: some View {
|
private var offlineSyncDetail: some View {
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ enum SettingsCategory: String, CaseIterable, Identifiable {
|
|||||||
case general
|
case general
|
||||||
case account
|
case account
|
||||||
case workspace
|
case workspace
|
||||||
case integrationsInstallation
|
|
||||||
|
|
||||||
var id: String { rawValue }
|
var id: String { rawValue }
|
||||||
|
|
||||||
@@ -17,19 +16,20 @@ enum SettingsCategory: String, CaseIterable, Identifiable {
|
|||||||
case .general: return "Outpost"
|
case .general: return "Outpost"
|
||||||
case .account: return "Account"
|
case .account: return "Account"
|
||||||
case .workspace: return "Workspace"
|
case .workspace: return "Workspace"
|
||||||
case .integrationsInstallation: return "Integrations & Installation"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One entry in the Settings sidebar. Mirrors Outline's own settings
|
/// One entry in the Settings sidebar. Mirrors Outline's own settings
|
||||||
/// categories (Account/Workspace/Integrations & Installation) so this app's
|
/// categories (Account/Workspace) so this app's settings read as a native
|
||||||
/// settings read as a native counterpart to the web app's, plus a `general`
|
/// counterpart to the web app's, plus a `general` group for things that are
|
||||||
/// group for things that are ours and don't map onto Outline's structure
|
/// ours and don't map onto Outline's structure (offline/sync, advanced,
|
||||||
/// (offline/sync, advanced, about, appearance).
|
/// about, appearance). Outline's own version info moved to the sidebar
|
||||||
|
/// footer (`SettingsSidebarList`) instead of a standalone
|
||||||
|
/// Integrations & Installation section.
|
||||||
///
|
///
|
||||||
/// Most of the Account/Workspace/Integrations cases are navigation-only for
|
/// Most of the Account/Workspace cases are navigation-only for now —
|
||||||
/// now — `SettingsView` renders a "Coming Soon" placeholder for anything not
|
/// `SettingsView` renders a "Coming Soon" placeholder for anything not
|
||||||
/// explicitly built yet. Content lands section by section.
|
/// explicitly built yet. Content lands section by section.
|
||||||
enum SettingsSection: String, CaseIterable, Identifiable, Hashable {
|
enum SettingsSection: String, CaseIterable, Identifiable, Hashable {
|
||||||
// General (ours)
|
// General (ours)
|
||||||
@@ -41,9 +41,6 @@ enum SettingsSection: String, CaseIterable, Identifiable, Hashable {
|
|||||||
// Workspace
|
// Workspace
|
||||||
case details, authentication, security, ai, members, groups, templates, emojis, applications, shared, links, webhooks, importData, exportData
|
case details, authentication, security, ai, members, groups, templates, emojis, applications, shared, links, webhooks, importData, exportData
|
||||||
|
|
||||||
// Integrations & Installation
|
|
||||||
case installation
|
|
||||||
|
|
||||||
var id: String { rawValue }
|
var id: String { rawValue }
|
||||||
|
|
||||||
var category: SettingsCategory {
|
var category: SettingsCategory {
|
||||||
@@ -54,8 +51,6 @@ enum SettingsSection: String, CaseIterable, Identifiable, Hashable {
|
|||||||
return .account
|
return .account
|
||||||
case .details, .authentication, .security, .ai, .members, .groups, .templates, .emojis, .applications, .shared, .links, .webhooks, .importData, .exportData:
|
case .details, .authentication, .security, .ai, .members, .groups, .templates, .emojis, .applications, .shared, .links, .webhooks, .importData, .exportData:
|
||||||
return .workspace
|
return .workspace
|
||||||
case .installation:
|
|
||||||
return .integrationsInstallation
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,7 +79,6 @@ enum SettingsSection: String, CaseIterable, Identifiable, Hashable {
|
|||||||
case .webhooks: return "Webhooks"
|
case .webhooks: return "Webhooks"
|
||||||
case .importData: return "Import"
|
case .importData: return "Import"
|
||||||
case .exportData: return "Export"
|
case .exportData: return "Export"
|
||||||
case .installation: return "Installation"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,16 +107,15 @@ enum SettingsSection: String, CaseIterable, Identifiable, Hashable {
|
|||||||
case .webhooks: return "bolt.horizontal"
|
case .webhooks: return "bolt.horizontal"
|
||||||
case .importData: return "square.and.arrow.down"
|
case .importData: return "square.and.arrow.down"
|
||||||
case .exportData: return "square.and.arrow.up"
|
case .exportData: return "square.and.arrow.up"
|
||||||
case .installation: return "shippingbox"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Everything actually built so far — everything else in Account/
|
/// Everything actually built so far — everything else in Account/
|
||||||
/// Workspace/Integrations & Installation renders a "Coming Soon"
|
/// Workspace renders a "Coming Soon" placeholder until its content is
|
||||||
/// placeholder until its content is specified and built.
|
/// specified and built.
|
||||||
var isImplemented: Bool {
|
var isImplemented: Bool {
|
||||||
switch self {
|
switch self {
|
||||||
case .appearance, .offlineSync, .advanced, .about, .profile, .preferences, .notifications, .passkeys, .apiAccess, .installation:
|
case .appearance, .offlineSync, .advanced, .about, .profile, .preferences, .notifications, .passkeys, .apiAccess:
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
|
|||||||
Reference in New Issue
Block a user