From c2b41ca96067076763e1808767dd5824950d6b5a Mon Sep 17 00:00:00 2001 From: psavarmattas Date: Fri, 14 Aug 2026 16:46:40 +0100 Subject: [PATCH] fix(sidebar): auto-refresh after creating a doc from Home or the reader toolbar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The reader's inline "New Document" button and Home's New Document sheet only navigated to the new document — neither had a handle on the sidebar row it landed under, so the tree stayed stale until the periodic remote-changes poll (45s) surfaced the "reload" banner. New Document flows that already have a direct handle on their own sidebar row (right-click a collection, right-click a document) already refreshed correctly and are untouched. Threads a documentsChangedToken from ContentView_macOS down through CollectionsTreeView -> CollectionTreeRow -> CollectionDocumentsOutline as externalRefreshToken; every expanded row reloads itself when it bumps, since neither Home nor the reader knows which row (if any) corresponds to where the new document landed. --- .../Collections/CollectionDocumentsOutline.swift | 14 +++++++++++++- .../Features/Collections/CollectionTreeRow.swift | 3 +++ .../Features/Collections/CollectionsTreeView.swift | 7 +++++++ .../Features/Collections/ContentView_macOS.swift | 11 +++++++++-- .../Features/Collections/DocumentReaderView.swift | 10 +++++++++- Outpost/Features/Home/HomeView.swift | 8 +++++++- 6 files changed, 48 insertions(+), 5 deletions(-) diff --git a/Outpost/Features/Collections/CollectionDocumentsOutline.swift b/Outpost/Features/Collections/CollectionDocumentsOutline.swift index 258b93b..1013033 100644 --- a/Outpost/Features/Collections/CollectionDocumentsOutline.swift +++ b/Outpost/Features/Collections/CollectionDocumentsOutline.swift @@ -12,6 +12,13 @@ struct CollectionDocumentsOutline: View { @State private var viewModel: DocumentsViewModel let sortOption: SidebarSortOption let refreshToken: Int + /// Bumped from `ContentView_macOS` whenever a document is created from + /// somewhere that has no direct handle on this row — the reader's + /// toolbar "New Document" button and Home's, specifically. Those can't + /// call `onDocumentsChanged()` the way a same-row sheet does, since they + /// don't know which (if any) sidebar row corresponds to where the new + /// document landed, so every expanded row just reloads itself. + let externalRefreshToken: Int let selectedDocumentID: String? /// Full chain from root to the clicked document (inclusive) — lets the /// toolbar render the real hierarchy instead of just the leaf title. @@ -26,6 +33,7 @@ struct CollectionDocumentsOutline: View { collection: OutlineCollection, sortOption: SidebarSortOption, refreshToken: Int, + externalRefreshToken: Int, selectedDocumentID: String?, onSelectDocument: @escaping ([OutlineDocument]) -> Void ) { @@ -33,6 +41,7 @@ struct CollectionDocumentsOutline: View { _viewModel = State(initialValue: DocumentsViewModel(apiClient: apiClient, collection: collection)) self.sortOption = sortOption self.refreshToken = refreshToken + self.externalRefreshToken = externalRefreshToken self.selectedDocumentID = selectedDocumentID self.onSelectDocument = onSelectDocument } @@ -62,7 +71,10 @@ struct CollectionDocumentsOutline: View { } } } - .task(id: refreshToken) { await viewModel.load() } + // Combined into one identity rather than two separate `.task(id:)` + // modifiers — each of those fires once unconditionally on first + // appear, so two of them would double the initial load. + .task(id: "\(refreshToken)-\(externalRefreshToken)") { await viewModel.load() } } } diff --git a/Outpost/Features/Collections/CollectionTreeRow.swift b/Outpost/Features/Collections/CollectionTreeRow.swift index a78deef..515363b 100644 --- a/Outpost/Features/Collections/CollectionTreeRow.swift +++ b/Outpost/Features/Collections/CollectionTreeRow.swift @@ -11,6 +11,8 @@ struct CollectionTreeRow: View { let isExpanded: Bool let isSelected: Bool let selectedDocumentID: String? + /// See the identical parameter on `CollectionDocumentsOutline`. + let externalRefreshToken: Int let onToggle: () -> Void let onSelectDocument: ([OutlineDocument]) -> Void let onSearchInCollection: (OutlineCollection) -> Void @@ -63,6 +65,7 @@ struct CollectionTreeRow: View { collection: collection, sortOption: sortOption, refreshToken: documentsRefreshToken, + externalRefreshToken: externalRefreshToken, selectedDocumentID: selectedDocumentID, onSelectDocument: onSelectDocument ) diff --git a/Outpost/Features/Collections/CollectionsTreeView.swift b/Outpost/Features/Collections/CollectionsTreeView.swift index 6760297..941801a 100644 --- a/Outpost/Features/Collections/CollectionsTreeView.swift +++ b/Outpost/Features/Collections/CollectionsTreeView.swift @@ -7,6 +7,10 @@ struct CollectionsTreeView: View { @Binding private var selectedCollection: OutlineCollection? let selectedDocumentID: String? @State private var expandedCollectionIDs: Set = [] + /// Bumped from `ContentView_macOS` whenever a document is created + /// somewhere with no direct handle on the sidebar row it belongs + /// under — see the identical parameter on `CollectionDocumentsOutline`. + let externalRefreshToken: Int let onSelectDocument: (OutlineCollection, [OutlineDocument]) -> Void let onSearchInCollection: (OutlineCollection) -> Void @@ -14,12 +18,14 @@ struct CollectionsTreeView: View { apiClient: OutlineAPIClient, selectedCollection: Binding, selectedDocumentID: String?, + externalRefreshToken: Int, onSelectDocument: @escaping (OutlineCollection, [OutlineDocument]) -> Void, onSearchInCollection: @escaping (OutlineCollection) -> Void ) { _viewModel = State(initialValue: CollectionsViewModel(apiClient: apiClient)) _selectedCollection = selectedCollection self.selectedDocumentID = selectedDocumentID + self.externalRefreshToken = externalRefreshToken self.onSelectDocument = onSelectDocument self.onSearchInCollection = onSearchInCollection } @@ -81,6 +87,7 @@ struct CollectionsTreeView: View { isExpanded: expandedCollectionIDs.contains(collection.id), isSelected: selectedCollection?.id == collection.id, selectedDocumentID: selectedDocumentID, + externalRefreshToken: externalRefreshToken, onToggle: { toggle(collection) }, onSelectDocument: { chain in onSelectDocument(collection, chain) }, onSearchInCollection: onSearchInCollection, diff --git a/Outpost/Features/Collections/ContentView_macOS.swift b/Outpost/Features/Collections/ContentView_macOS.swift index 610f93b..89595fe 100644 --- a/Outpost/Features/Collections/ContentView_macOS.swift +++ b/Outpost/Features/Collections/ContentView_macOS.swift @@ -16,6 +16,11 @@ struct ContentView_macOS: View { @State private var contextualSearchQuery = "" @State private var isContextualSearchExpanded = false @FocusState private var isContextualSearchFocused: Bool + /// Bumped whenever a document is created from somewhere with no direct + /// handle on the sidebar row it belongs under (the reader toolbar's and + /// Home's "New Document" buttons) — every expanded sidebar row reloads + /// itself in response. See `CollectionDocumentsOutline.externalRefreshToken`. + @State private var documentsChangedToken = 0 private var trimmedGlobalQuery: String { globalSearchQuery.trimmingCharacters(in: .whitespacesAndNewlines) @@ -191,6 +196,7 @@ struct ContentView_macOS: View { apiClient: apiClient, selectedCollection: $selectedCollection, selectedDocumentID: documentPath.last?.id, + externalRefreshToken: documentsChangedToken, onSelectDocument: selectDocumentChain, onSearchInCollection: searchInCollection ) @@ -244,7 +250,7 @@ struct ContentView_macOS: View { if !trimmedGlobalQuery.isEmpty { GlobalSearchResultsView(apiClient: apiClient, query: trimmedGlobalQuery, onOpenDocument: openDocument) } else if isShowingHome { - HomeView(apiClient: apiClient, onOpenDocument: openDocument) + HomeView(apiClient: apiClient, onOpenDocument: openDocument, onDocumentCreated: { documentsChangedToken += 1 }) } else if let selectedCollection { CollectionOverviewView( apiClient: apiClient, @@ -269,7 +275,8 @@ struct ContentView_macOS: View { if !documentPath.isEmpty { documentPath.removeLast() } - } + }, + onDocumentCreated: { documentsChangedToken += 1 } ) } } diff --git a/Outpost/Features/Collections/DocumentReaderView.swift b/Outpost/Features/Collections/DocumentReaderView.swift index 0772981..3ba40aa 100644 --- a/Outpost/Features/Collections/DocumentReaderView.swift +++ b/Outpost/Features/Collections/DocumentReaderView.swift @@ -23,6 +23,11 @@ struct DocumentReaderView: View { /// longer visible in the collection it was opened from, so the reader /// pops itself off the navigation stack. let onDeleted: () -> Void + /// The reader's own "New Document" toolbar button has no direct handle + /// on the sidebar row it belongs under — this tells the sidebar a + /// document exists now so it can pick it up. See + /// `CollectionDocumentsOutline.externalRefreshToken`. + let onDocumentCreated: () -> Void @State private var isShowingUnpublishConfirmation = false @State private var isShowingArchiveConfirmation = false @@ -40,13 +45,15 @@ struct DocumentReaderView: View { apiClient: OutlineAPIClient, document: OutlineDocument, onOpenChild: @escaping (OutlineDocument) -> Void, - onDeleted: @escaping () -> Void + onDeleted: @escaping () -> Void, + onDocumentCreated: @escaping () -> Void ) { self.apiClient = apiClient self.document = document _viewModel = State(initialValue: DocumentReaderViewModel(apiClient: apiClient, document: document)) self.onOpenChild = onOpenChild self.onDeleted = onDeleted + self.onDocumentCreated = onDocumentCreated } var body: some View { @@ -213,6 +220,7 @@ struct DocumentReaderView: View { } .sheet(isPresented: $isShowingNewDocumentSheet) { NewDocumentSheet(apiClient: apiClient, initialParentDocument: document) { child in + onDocumentCreated() onOpenChild(child) } } diff --git a/Outpost/Features/Home/HomeView.swift b/Outpost/Features/Home/HomeView.swift index ffc868f..1866af7 100644 --- a/Outpost/Features/Home/HomeView.swift +++ b/Outpost/Features/Home/HomeView.swift @@ -5,6 +5,10 @@ import OutlineKit struct HomeView: View { let apiClient: OutlineAPIClient let onOpenDocument: (OutlineDocument) -> Void + /// Home has no sidebar row of its own to reload directly — this tells + /// the sidebar a document exists now so it can pick it up. See + /// `CollectionDocumentsOutline.externalRefreshToken`. + let onDocumentCreated: () -> Void @State private var viewModel: HomeViewModel @State private var selectedTab: HomeTab = .recentlyViewed @@ -13,9 +17,10 @@ struct HomeView: View { private let pinnedGridColumns = [GridItem(.adaptive(minimum: 260), spacing: 8)] private let tabGridColumns = [GridItem(.adaptive(minimum: 220), spacing: 12)] - init(apiClient: OutlineAPIClient, onOpenDocument: @escaping (OutlineDocument) -> Void) { + init(apiClient: OutlineAPIClient, onOpenDocument: @escaping (OutlineDocument) -> Void, onDocumentCreated: @escaping () -> Void) { self.apiClient = apiClient self.onOpenDocument = onOpenDocument + self.onDocumentCreated = onDocumentCreated _viewModel = State(initialValue: HomeViewModel(apiClient: apiClient)) } @@ -51,6 +56,7 @@ struct HomeView: View { } .sheet(isPresented: $isShowingNewDocumentSheet) { NewDocumentSheet(apiClient: apiClient) { document in + onDocumentCreated() onOpenDocument(document) } }