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

The reader's inline "New Document" button (createChildDocument())
only navigated to the new document — no handle on the sidebar row it
landed under, so the tree stayed stale until the periodic
remote-changes poll surfaced the reload banner.

Right-click "New Document" on a collection already refreshed correctly
(bumps its own documentsRefreshToken) and is untouched.

Threads a documentsChangedToken from ContentView_macOS down through
CollectionsTreeView -> CollectionTreeRow -> CollectionDocumentsOutline
as externalRefreshToken; every expanded row reloads itself when it
bumps, since the reader doesn't know which row (if any) corresponds to
where the new document landed.
This commit is contained in:
2026-08-14 16:50:09 +01:00
parent ad3e35e28e
commit ed9fd26c85
5 changed files with 39 additions and 3 deletions
@@ -13,6 +13,12 @@ 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, specifically, which doesn't know which
/// (if any) sidebar row corresponds to the collection its new document
/// landed in, 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.
@@ -32,6 +38,7 @@ struct CollectionDocumentsOutline: View {
collection: OutlineCollection,
sortOption: SidebarSortOption,
refreshToken: Int,
externalRefreshToken: Int,
selectedDocumentID: String?,
onSelectDocument: @escaping ([OutlineDocument]) -> Void
) {
@@ -40,6 +47,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
}
@@ -71,7 +79,10 @@ struct CollectionDocumentsOutline: View {
}
}
}
.task(id: refreshToken) {
// 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()
await loadPins()
}
@@ -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
@@ -62,6 +64,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,
@@ -12,6 +12,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
/// "New Document" button) every expanded sidebar row reloads itself
/// in response. See `CollectionDocumentsOutline.externalRefreshToken`.
@State private var documentsChangedToken = 0
private var trimmedGlobalQuery: String {
globalSearchQuery.trimmingCharacters(in: .whitespacesAndNewlines)
@@ -153,6 +158,7 @@ struct ContentView_macOS: View {
apiClient: apiClient,
selectedCollection: $selectedCollection,
selectedDocumentID: documentPath.last?.id,
externalRefreshToken: documentsChangedToken,
onSelectDocument: selectDocumentChain,
onSearchInCollection: searchInCollection
)
@@ -229,7 +235,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
@@ -39,13 +44,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 {
@@ -464,6 +471,7 @@ struct DocumentReaderView: View {
let child = try await apiClient.createDocument(
CreateDocumentRequest(title: "Untitled", text: "", collectionId: collectionId, parentDocumentId: viewModel.documentId)
)
onDocumentCreated()
onOpenChild(child)
} catch {
actionErrorMessage = outlineErrorMessage(error, fallback: "Couldn't create a new document.")