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:
@@ -12,6 +12,13 @@ struct CollectionDocumentsOutline: View {
|
|||||||
@State private var viewModel: DocumentsViewModel
|
@State private var viewModel: DocumentsViewModel
|
||||||
let sortOption: SidebarSortOption
|
let sortOption: SidebarSortOption
|
||||||
let refreshToken: Int
|
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?
|
let selectedDocumentID: String?
|
||||||
/// Full chain from root to the clicked document (inclusive) — lets the
|
/// Full chain from root to the clicked document (inclusive) — lets the
|
||||||
/// toolbar render the real hierarchy instead of just the leaf title.
|
/// toolbar render the real hierarchy instead of just the leaf title.
|
||||||
@@ -26,6 +33,7 @@ struct CollectionDocumentsOutline: View {
|
|||||||
collection: OutlineCollection,
|
collection: OutlineCollection,
|
||||||
sortOption: SidebarSortOption,
|
sortOption: SidebarSortOption,
|
||||||
refreshToken: Int,
|
refreshToken: Int,
|
||||||
|
externalRefreshToken: Int,
|
||||||
selectedDocumentID: String?,
|
selectedDocumentID: String?,
|
||||||
onSelectDocument: @escaping ([OutlineDocument]) -> Void
|
onSelectDocument: @escaping ([OutlineDocument]) -> Void
|
||||||
) {
|
) {
|
||||||
@@ -33,6 +41,7 @@ struct CollectionDocumentsOutline: View {
|
|||||||
_viewModel = State(initialValue: DocumentsViewModel(apiClient: apiClient, collection: collection))
|
_viewModel = State(initialValue: DocumentsViewModel(apiClient: apiClient, collection: collection))
|
||||||
self.sortOption = sortOption
|
self.sortOption = sortOption
|
||||||
self.refreshToken = refreshToken
|
self.refreshToken = refreshToken
|
||||||
|
self.externalRefreshToken = externalRefreshToken
|
||||||
self.selectedDocumentID = selectedDocumentID
|
self.selectedDocumentID = selectedDocumentID
|
||||||
self.onSelectDocument = onSelectDocument
|
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 isExpanded: Bool
|
||||||
let isSelected: Bool
|
let isSelected: Bool
|
||||||
let selectedDocumentID: String?
|
let selectedDocumentID: String?
|
||||||
|
/// See the identical parameter on `CollectionDocumentsOutline`.
|
||||||
|
let externalRefreshToken: Int
|
||||||
let onToggle: () -> Void
|
let onToggle: () -> Void
|
||||||
let onSelectDocument: ([OutlineDocument]) -> Void
|
let onSelectDocument: ([OutlineDocument]) -> Void
|
||||||
let onSearchInCollection: (OutlineCollection) -> Void
|
let onSearchInCollection: (OutlineCollection) -> Void
|
||||||
@@ -63,6 +65,7 @@ struct CollectionTreeRow: View {
|
|||||||
collection: collection,
|
collection: collection,
|
||||||
sortOption: sortOption,
|
sortOption: sortOption,
|
||||||
refreshToken: documentsRefreshToken,
|
refreshToken: documentsRefreshToken,
|
||||||
|
externalRefreshToken: externalRefreshToken,
|
||||||
selectedDocumentID: selectedDocumentID,
|
selectedDocumentID: selectedDocumentID,
|
||||||
onSelectDocument: onSelectDocument
|
onSelectDocument: onSelectDocument
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ struct CollectionsTreeView: View {
|
|||||||
@Binding private var selectedCollection: OutlineCollection?
|
@Binding private var selectedCollection: OutlineCollection?
|
||||||
let selectedDocumentID: String?
|
let selectedDocumentID: String?
|
||||||
@State private var expandedCollectionIDs: Set<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 onSelectDocument: (OutlineCollection, [OutlineDocument]) -> Void
|
||||||
let onSearchInCollection: (OutlineCollection) -> Void
|
let onSearchInCollection: (OutlineCollection) -> Void
|
||||||
|
|
||||||
@@ -14,12 +18,14 @@ struct CollectionsTreeView: View {
|
|||||||
apiClient: OutlineAPIClient,
|
apiClient: OutlineAPIClient,
|
||||||
selectedCollection: Binding<OutlineCollection?>,
|
selectedCollection: Binding<OutlineCollection?>,
|
||||||
selectedDocumentID: String?,
|
selectedDocumentID: String?,
|
||||||
|
externalRefreshToken: Int,
|
||||||
onSelectDocument: @escaping (OutlineCollection, [OutlineDocument]) -> Void,
|
onSelectDocument: @escaping (OutlineCollection, [OutlineDocument]) -> Void,
|
||||||
onSearchInCollection: @escaping (OutlineCollection) -> Void
|
onSearchInCollection: @escaping (OutlineCollection) -> Void
|
||||||
) {
|
) {
|
||||||
_viewModel = State(initialValue: CollectionsViewModel(apiClient: apiClient))
|
_viewModel = State(initialValue: CollectionsViewModel(apiClient: apiClient))
|
||||||
_selectedCollection = selectedCollection
|
_selectedCollection = selectedCollection
|
||||||
self.selectedDocumentID = selectedDocumentID
|
self.selectedDocumentID = selectedDocumentID
|
||||||
|
self.externalRefreshToken = externalRefreshToken
|
||||||
self.onSelectDocument = onSelectDocument
|
self.onSelectDocument = onSelectDocument
|
||||||
self.onSearchInCollection = onSearchInCollection
|
self.onSearchInCollection = onSearchInCollection
|
||||||
}
|
}
|
||||||
@@ -81,6 +87,7 @@ struct CollectionsTreeView: View {
|
|||||||
isExpanded: expandedCollectionIDs.contains(collection.id),
|
isExpanded: expandedCollectionIDs.contains(collection.id),
|
||||||
isSelected: selectedCollection?.id == collection.id,
|
isSelected: selectedCollection?.id == collection.id,
|
||||||
selectedDocumentID: selectedDocumentID,
|
selectedDocumentID: selectedDocumentID,
|
||||||
|
externalRefreshToken: externalRefreshToken,
|
||||||
onToggle: { toggle(collection) },
|
onToggle: { toggle(collection) },
|
||||||
onSelectDocument: { chain in onSelectDocument(collection, chain) },
|
onSelectDocument: { chain in onSelectDocument(collection, chain) },
|
||||||
onSearchInCollection: onSearchInCollection,
|
onSearchInCollection: onSearchInCollection,
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ struct ContentView_macOS: View {
|
|||||||
@State private var contextualSearchQuery = ""
|
@State private var contextualSearchQuery = ""
|
||||||
@State private var isContextualSearchExpanded = false
|
@State private var isContextualSearchExpanded = false
|
||||||
@FocusState private var isContextualSearchFocused: Bool
|
@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 {
|
private var trimmedGlobalQuery: String {
|
||||||
globalSearchQuery.trimmingCharacters(in: .whitespacesAndNewlines)
|
globalSearchQuery.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
@@ -191,6 +196,7 @@ struct ContentView_macOS: View {
|
|||||||
apiClient: apiClient,
|
apiClient: apiClient,
|
||||||
selectedCollection: $selectedCollection,
|
selectedCollection: $selectedCollection,
|
||||||
selectedDocumentID: documentPath.last?.id,
|
selectedDocumentID: documentPath.last?.id,
|
||||||
|
externalRefreshToken: documentsChangedToken,
|
||||||
onSelectDocument: selectDocumentChain,
|
onSelectDocument: selectDocumentChain,
|
||||||
onSearchInCollection: searchInCollection
|
onSearchInCollection: searchInCollection
|
||||||
)
|
)
|
||||||
@@ -244,7 +250,7 @@ struct ContentView_macOS: View {
|
|||||||
if !trimmedGlobalQuery.isEmpty {
|
if !trimmedGlobalQuery.isEmpty {
|
||||||
GlobalSearchResultsView(apiClient: apiClient, query: trimmedGlobalQuery, onOpenDocument: openDocument)
|
GlobalSearchResultsView(apiClient: apiClient, query: trimmedGlobalQuery, onOpenDocument: openDocument)
|
||||||
} else if isShowingHome {
|
} else if isShowingHome {
|
||||||
HomeView(apiClient: apiClient, onOpenDocument: openDocument)
|
HomeView(apiClient: apiClient, onOpenDocument: openDocument, onDocumentCreated: { documentsChangedToken += 1 })
|
||||||
} else if let selectedCollection {
|
} else if let selectedCollection {
|
||||||
CollectionOverviewView(
|
CollectionOverviewView(
|
||||||
apiClient: apiClient,
|
apiClient: apiClient,
|
||||||
@@ -269,7 +275,8 @@ struct ContentView_macOS: View {
|
|||||||
if !documentPath.isEmpty {
|
if !documentPath.isEmpty {
|
||||||
documentPath.removeLast()
|
documentPath.removeLast()
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
onDocumentCreated: { documentsChangedToken += 1 }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,11 @@ struct DocumentReaderView: View {
|
|||||||
/// longer visible in the collection it was opened from, so the reader
|
/// longer visible in the collection it was opened from, so the reader
|
||||||
/// pops itself off the navigation stack.
|
/// pops itself off the navigation stack.
|
||||||
let onDeleted: () -> Void
|
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 isShowingUnpublishConfirmation = false
|
||||||
@State private var isShowingArchiveConfirmation = false
|
@State private var isShowingArchiveConfirmation = false
|
||||||
@@ -40,13 +45,15 @@ struct DocumentReaderView: View {
|
|||||||
apiClient: OutlineAPIClient,
|
apiClient: OutlineAPIClient,
|
||||||
document: OutlineDocument,
|
document: OutlineDocument,
|
||||||
onOpenChild: @escaping (OutlineDocument) -> Void,
|
onOpenChild: @escaping (OutlineDocument) -> Void,
|
||||||
onDeleted: @escaping () -> Void
|
onDeleted: @escaping () -> Void,
|
||||||
|
onDocumentCreated: @escaping () -> Void
|
||||||
) {
|
) {
|
||||||
self.apiClient = apiClient
|
self.apiClient = apiClient
|
||||||
self.document = document
|
self.document = document
|
||||||
_viewModel = State(initialValue: DocumentReaderViewModel(apiClient: apiClient, document: document))
|
_viewModel = State(initialValue: DocumentReaderViewModel(apiClient: apiClient, document: document))
|
||||||
self.onOpenChild = onOpenChild
|
self.onOpenChild = onOpenChild
|
||||||
self.onDeleted = onDeleted
|
self.onDeleted = onDeleted
|
||||||
|
self.onDocumentCreated = onDocumentCreated
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -213,6 +220,7 @@ struct DocumentReaderView: View {
|
|||||||
}
|
}
|
||||||
.sheet(isPresented: $isShowingNewDocumentSheet) {
|
.sheet(isPresented: $isShowingNewDocumentSheet) {
|
||||||
NewDocumentSheet(apiClient: apiClient, initialParentDocument: document) { child in
|
NewDocumentSheet(apiClient: apiClient, initialParentDocument: document) { child in
|
||||||
|
onDocumentCreated()
|
||||||
onOpenChild(child)
|
onOpenChild(child)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ import OutlineKit
|
|||||||
struct HomeView: View {
|
struct HomeView: View {
|
||||||
let apiClient: OutlineAPIClient
|
let apiClient: OutlineAPIClient
|
||||||
let onOpenDocument: (OutlineDocument) -> Void
|
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 viewModel: HomeViewModel
|
||||||
@State private var selectedTab: HomeTab = .recentlyViewed
|
@State private var selectedTab: HomeTab = .recentlyViewed
|
||||||
@@ -13,9 +17,10 @@ struct HomeView: View {
|
|||||||
private let pinnedGridColumns = [GridItem(.adaptive(minimum: 260), spacing: 8)]
|
private let pinnedGridColumns = [GridItem(.adaptive(minimum: 260), spacing: 8)]
|
||||||
private let tabGridColumns = [GridItem(.adaptive(minimum: 220), spacing: 12)]
|
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.apiClient = apiClient
|
||||||
self.onOpenDocument = onOpenDocument
|
self.onOpenDocument = onOpenDocument
|
||||||
|
self.onDocumentCreated = onDocumentCreated
|
||||||
_viewModel = State(initialValue: HomeViewModel(apiClient: apiClient))
|
_viewModel = State(initialValue: HomeViewModel(apiClient: apiClient))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,6 +56,7 @@ struct HomeView: View {
|
|||||||
}
|
}
|
||||||
.sheet(isPresented: $isShowingNewDocumentSheet) {
|
.sheet(isPresented: $isShowingNewDocumentSheet) {
|
||||||
NewDocumentSheet(apiClient: apiClient) { document in
|
NewDocumentSheet(apiClient: apiClient) { document in
|
||||||
|
onDocumentCreated()
|
||||||
onOpenDocument(document)
|
onOpenDocument(document)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user