Remember previous location (Preferences): persists collection + document chain (or Home) to UserDefaults on every navigation change, gated on the preference. Restored once per launch by resolving the stored IDs back through the API, stopping at the first failure (deleted doc, offline, etc.) rather than aborting the whole restore — a partial chain beats falling all the way back to Home. Cleared on sign-out so switching accounts can't restore a stale location. Command Palette (new Settings → Editor section, not synced to Outline): ⌘K opens a floating overlay, arrow-key/click navigation, Enter or click to select. Always searches locally, never a per-keystroke network request: - Lightweight (default): live listCollections + listViewedDocuments fetch once on open. - Full Workspace (opt-in, requires Full Local Sync on): reads CachingOutlineAPIClient's local cache directly — zero network calls, includes nested sub-documents now that Full Local Sync actually caches them (see the paired OutlineKit commit). Fixed through live testing, in order found: - Focus: the search field wasn't reliably first responder the instant ⌘K opened it (also the likely source of several AppKit CA-transaction console warnings) — added a short delay before focusing. - Full Workspace "no results": was sequential one-collection-at-a-time fetching before the cache-read redesign: withTaskGroup made it concurrent, and reading from the cache instead of the network made it moot. - Arrow keys not moving selection: .onKeyPress was on the outer card, but the focused TextField swallowed the events before they could bubble up. Moved the handlers directly onto the TextField. - Search ranking: exact-phrase-only matching meant a title like "Test Plan Document" never matched a "test document" query at all (filtered out, not just ranked low), making it look like collections always won. Added a fallback tier: every word of a multi-word query present anywhere in the title still matches, ranked below exact/prefix/substring hits. - foregroundStyle(.secondary vs .orange) ternary: HierarchicalShapeStyle vs Color type mismatch, fixed with AnyShapeStyle on both branches.
142 lines
5.5 KiB
Swift
142 lines
5.5 KiB
Swift
import Observation
|
|
|
|
/// Top-level groupings shown as section headers in `SettingsSidebarList`.
|
|
/// `general` holds everything that's ours, not Outline's own settings
|
|
/// categories — labeled "Outpost" so it reads as clearly distinct from the
|
|
/// Outline-sourced groups below it.
|
|
enum SettingsCategory: String, CaseIterable, Identifiable {
|
|
case general
|
|
case account
|
|
case workspace
|
|
|
|
var id: String { rawValue }
|
|
|
|
var title: String? {
|
|
switch self {
|
|
case .general: return "Outpost"
|
|
case .account: return "Account"
|
|
case .workspace: return "Workspace"
|
|
}
|
|
}
|
|
}
|
|
|
|
/// One entry in the Settings sidebar. Mirrors Outline's own settings
|
|
/// categories (Account/Workspace) so this app's settings read as a native
|
|
/// counterpart to the web app's, plus a `general` group for things that are
|
|
/// ours and don't map onto Outline's structure (offline/sync, advanced,
|
|
/// 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 cases are navigation-only for now —
|
|
/// `SettingsView` renders a "Coming Soon" placeholder for anything not
|
|
/// explicitly built yet. Content lands section by section.
|
|
enum SettingsSection: String, CaseIterable, Identifiable, Hashable {
|
|
// General (ours)
|
|
case appearance, editor, offlineSync, advanced, about
|
|
|
|
// Account
|
|
case profile, preferences, notifications, passkeys, apiAccess
|
|
|
|
// Workspace
|
|
case details, authentication, security, ai, members, groups, templates, emojis, applications, shared, links, webhooks, importData, exportData
|
|
|
|
var id: String { rawValue }
|
|
|
|
var category: SettingsCategory {
|
|
switch self {
|
|
case .appearance, .editor, .offlineSync, .advanced, .about:
|
|
return .general
|
|
case .profile, .preferences, .notifications, .passkeys, .apiAccess:
|
|
return .account
|
|
case .details, .authentication, .security, .ai, .members, .groups, .templates, .emojis, .applications, .shared, .links, .webhooks, .importData, .exportData:
|
|
return .workspace
|
|
}
|
|
}
|
|
|
|
var title: String {
|
|
switch self {
|
|
case .appearance: return "Appearance"
|
|
case .editor: return "Editor"
|
|
case .offlineSync: return "Offline & Sync"
|
|
case .advanced: return "Advanced"
|
|
case .about: return "About"
|
|
case .profile: return "Profile"
|
|
case .preferences: return "Preferences"
|
|
case .notifications: return "Notifications"
|
|
case .passkeys: return "Passkeys"
|
|
case .apiAccess: return "API & Access"
|
|
case .details: return "Details"
|
|
case .authentication: return "Authentication"
|
|
case .security: return "Security"
|
|
case .ai: return "AI"
|
|
case .members: return "Members"
|
|
case .groups: return "Groups"
|
|
case .templates: return "Templates"
|
|
case .emojis: return "Emojis"
|
|
case .applications: return "Applications"
|
|
case .shared: return "Shared"
|
|
case .links: return "Links"
|
|
case .webhooks: return "Webhooks"
|
|
case .importData: return "Import"
|
|
case .exportData: return "Export"
|
|
}
|
|
}
|
|
|
|
var icon: String {
|
|
switch self {
|
|
case .appearance: return "paintbrush"
|
|
case .editor: return "square.split.2x1"
|
|
case .offlineSync: return "arrow.triangle.2.circlepath"
|
|
case .advanced: return "wrench.and.screwdriver"
|
|
case .about: return "info.circle"
|
|
case .profile: return "person.crop.circle"
|
|
case .preferences: return "gearshape"
|
|
case .notifications: return "bell"
|
|
case .passkeys: return "key"
|
|
case .apiAccess: return "chevron.left.forwardslash.chevron.right"
|
|
case .details: return "building.2"
|
|
case .authentication: return "lock"
|
|
case .security: return "shield"
|
|
case .ai: return "sparkles"
|
|
case .members: return "person.2"
|
|
case .groups: return "person.3"
|
|
case .templates: return "doc.on.doc"
|
|
case .emojis: return "face.smiling"
|
|
case .applications: return "app.badge"
|
|
case .shared: return "square.and.arrow.up.on.square"
|
|
case .links: return "link"
|
|
case .webhooks: return "bolt.horizontal"
|
|
case .importData: return "square.and.arrow.down"
|
|
case .exportData: return "square.and.arrow.up"
|
|
}
|
|
}
|
|
|
|
/// Everything actually built so far — everything else in Account/
|
|
/// Workspace renders a "Coming Soon" placeholder until its content is
|
|
/// specified and built.
|
|
var isImplemented: Bool {
|
|
switch self {
|
|
case .appearance, .editor, .offlineSync, .advanced, .about, .profile, .preferences, .notifications, .passkeys, .apiAccess:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Cross-cutting UI state that doesn't belong to any one screen. Lives at
|
|
/// `OutpostApp` (so both `RootView`'s content and its `⌘,` command can reach
|
|
/// it) and is read wherever something needs to open Settings (the profile
|
|
/// menu) or render it — as a swap of the *existing* sidebar/detail panes in
|
|
/// `ContentView_macOS`, not a separate popup window or an overlay that hides
|
|
/// the sidebar.
|
|
@Observable
|
|
@MainActor
|
|
final class AppNavigation {
|
|
var isShowingSettings = false
|
|
var selectedSettingsSection: SettingsSection? = .appearance
|
|
/// ⌘K, see `OutpostApp`'s `CommandGroup` and `CommandPaletteView`.
|
|
var isShowingCommandPalette = false
|
|
}
|