fix(sidebar): auto-refresh after creating a doc from Home or the reader toolbar

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.
This commit is contained in:
2026-08-14 16:46:40 +01:00
parent b45d72238c
commit c2b41ca960
6 changed files with 48 additions and 5 deletions
@@ -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() }
}
}
@@ -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
)
@@ -7,6 +7,10 @@ struct CollectionsTreeView: View {
@Binding private var selectedCollection: OutlineCollection?
let selectedDocumentID: String?
@State private var expandedCollectionIDs: Set<String> = []
/// 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<OutlineCollection?>,
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,
@@ -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 }
)
}
}
@@ -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)
}
}
+7 -1
View File
@@ -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)
}
}