feat(collections): show full document hierarchy in the toolbar
Sidebar clicks on a nested document now push the whole ancestor chain onto documentPath at once (via DocumentNode, which already has it from the client-side tree) instead of just the leaf — that array is now the single source of truth for both the NavigationStack and the toolbar breadcrumb, so they can't drift out of sync the way a separate "ancestors" state did. Ancestors render icon-only in the breadcrumb; only the document actually being viewed gets its full title, so a deep chain doesn't overrun the toolbar. Flat-list and search-result clicks (no known ancestors) now go through the same onOpenDocument callback instead of a bare NavigationLink, so every document-opening path resets the chain consistently rather than risking a stale one bleeding through. Opening a sub-document from inside the reader itself is a genuine stack append (correct back-button semantics: back returns to the parent's own reader, not the collection) rather than the reset-and-replace used for jumps between unrelated documents. Also renders a document's children in the reader, fetched via documents.list's parentDocumentId filter — previously not shown anywhere.
This commit is contained in:
@@ -10,7 +10,9 @@ struct CollectionDocumentsOutline: View {
|
|||||||
let sortOption: SidebarSortOption
|
let sortOption: SidebarSortOption
|
||||||
let refreshToken: Int
|
let refreshToken: Int
|
||||||
let selectedDocumentID: String?
|
let selectedDocumentID: String?
|
||||||
let onSelectDocument: (OutlineDocument) -> Void
|
/// Full chain from root to the clicked document (inclusive) — lets the
|
||||||
|
/// toolbar render the real hierarchy instead of just the leaf title.
|
||||||
|
let onSelectDocument: ([OutlineDocument]) -> Void
|
||||||
|
|
||||||
private var tree: [DocumentNode] {
|
private var tree: [DocumentNode] {
|
||||||
buildDocumentTree(from: viewModel.documents, sortedBy: sortOption)
|
buildDocumentTree(from: viewModel.documents, sortedBy: sortOption)
|
||||||
@@ -22,7 +24,7 @@ struct CollectionDocumentsOutline: View {
|
|||||||
sortOption: SidebarSortOption,
|
sortOption: SidebarSortOption,
|
||||||
refreshToken: Int,
|
refreshToken: Int,
|
||||||
selectedDocumentID: String?,
|
selectedDocumentID: String?,
|
||||||
onSelectDocument: @escaping (OutlineDocument) -> Void
|
onSelectDocument: @escaping ([OutlineDocument]) -> Void
|
||||||
) {
|
) {
|
||||||
_viewModel = State(initialValue: DocumentsViewModel(apiClient: apiClient, collection: collection))
|
_viewModel = State(initialValue: DocumentsViewModel(apiClient: apiClient, collection: collection))
|
||||||
self.sortOption = sortOption
|
self.sortOption = sortOption
|
||||||
@@ -47,6 +49,7 @@ struct CollectionDocumentsOutline: View {
|
|||||||
DocumentNodeRow(
|
DocumentNodeRow(
|
||||||
node: node,
|
node: node,
|
||||||
depth: 0,
|
depth: 0,
|
||||||
|
ancestors: [],
|
||||||
selectedDocumentID: selectedDocumentID,
|
selectedDocumentID: selectedDocumentID,
|
||||||
onSelectDocument: onSelectDocument
|
onSelectDocument: onSelectDocument
|
||||||
)
|
)
|
||||||
@@ -60,8 +63,10 @@ struct CollectionDocumentsOutline: View {
|
|||||||
private struct DocumentNodeRow: View {
|
private struct DocumentNodeRow: View {
|
||||||
let node: DocumentNode
|
let node: DocumentNode
|
||||||
let depth: Int
|
let depth: Int
|
||||||
|
/// Chain from root down to (not including) this node.
|
||||||
|
let ancestors: [OutlineDocument]
|
||||||
let selectedDocumentID: String?
|
let selectedDocumentID: String?
|
||||||
let onSelectDocument: (OutlineDocument) -> Void
|
let onSelectDocument: ([OutlineDocument]) -> Void
|
||||||
|
|
||||||
@State private var isExpanded = false
|
@State private var isExpanded = false
|
||||||
|
|
||||||
@@ -95,7 +100,7 @@ private struct DocumentNodeRow: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
onSelectDocument(node.document)
|
onSelectDocument(ancestors + [node.document])
|
||||||
} label: {
|
} label: {
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
if let emoji = node.document.emoji {
|
if let emoji = node.document.emoji {
|
||||||
@@ -129,6 +134,7 @@ private struct DocumentNodeRow: View {
|
|||||||
DocumentNodeRow(
|
DocumentNodeRow(
|
||||||
node: child,
|
node: child,
|
||||||
depth: depth + 1,
|
depth: depth + 1,
|
||||||
|
ancestors: ancestors + [node.document],
|
||||||
selectedDocumentID: selectedDocumentID,
|
selectedDocumentID: selectedDocumentID,
|
||||||
onSelectDocument: onSelectDocument
|
onSelectDocument: onSelectDocument
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -13,16 +13,23 @@ struct CollectionOverviewView: View {
|
|||||||
// only the resulting search state.
|
// only the resulting search state.
|
||||||
@State private var searchViewModel: DocumentTitleSearchViewModel
|
@State private var searchViewModel: DocumentTitleSearchViewModel
|
||||||
@Binding var searchQuery: String
|
@Binding var searchQuery: String
|
||||||
|
let onOpenDocument: (OutlineDocument) -> Void
|
||||||
|
|
||||||
private var trimmedSearchQuery: String {
|
private var trimmedSearchQuery: String {
|
||||||
searchQuery.trimmingCharacters(in: .whitespacesAndNewlines)
|
searchQuery.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
}
|
}
|
||||||
|
|
||||||
init(apiClient: OutlineAPIClient, collection: OutlineCollection, searchQuery: Binding<String>) {
|
init(
|
||||||
|
apiClient: OutlineAPIClient,
|
||||||
|
collection: OutlineCollection,
|
||||||
|
searchQuery: Binding<String>,
|
||||||
|
onOpenDocument: @escaping (OutlineDocument) -> Void
|
||||||
|
) {
|
||||||
self.collection = collection
|
self.collection = collection
|
||||||
_viewModel = State(initialValue: DocumentsViewModel(apiClient: apiClient, collection: collection))
|
_viewModel = State(initialValue: DocumentsViewModel(apiClient: apiClient, collection: collection))
|
||||||
_searchViewModel = State(initialValue: DocumentTitleSearchViewModel(apiClient: apiClient, collectionId: collection.id))
|
_searchViewModel = State(initialValue: DocumentTitleSearchViewModel(apiClient: apiClient, collectionId: collection.id))
|
||||||
_searchQuery = searchQuery
|
_searchQuery = searchQuery
|
||||||
|
self.onOpenDocument = onOpenDocument
|
||||||
}
|
}
|
||||||
|
|
||||||
private var sortedDocuments: [OutlineDocument] {
|
private var sortedDocuments: [OutlineDocument] {
|
||||||
@@ -127,9 +134,12 @@ struct CollectionOverviewView: View {
|
|||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
List(sortedDocuments) { document in
|
List(sortedDocuments) { document in
|
||||||
NavigationLink(value: document) {
|
Button {
|
||||||
|
onOpenDocument(document)
|
||||||
|
} label: {
|
||||||
DocumentRowView(document: document)
|
DocumentRowView(document: document)
|
||||||
}
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -149,7 +159,9 @@ struct CollectionOverviewView: View {
|
|||||||
ContentUnavailableView.search(text: trimmedSearchQuery)
|
ContentUnavailableView.search(text: trimmedSearchQuery)
|
||||||
} else {
|
} else {
|
||||||
List(searchViewModel.results) { result in
|
List(searchViewModel.results) { result in
|
||||||
NavigationLink(value: result.document) {
|
Button {
|
||||||
|
onOpenDocument(result.document)
|
||||||
|
} label: {
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
DocumentRowView(document: result.document)
|
DocumentRowView(document: result.document)
|
||||||
Text(result.context)
|
Text(result.context)
|
||||||
@@ -159,6 +171,7 @@ struct CollectionOverviewView: View {
|
|||||||
}
|
}
|
||||||
.padding(.vertical, 2)
|
.padding(.vertical, 2)
|
||||||
}
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ struct CollectionTreeRow: View {
|
|||||||
let isSelected: Bool
|
let isSelected: Bool
|
||||||
let selectedDocumentID: String?
|
let selectedDocumentID: String?
|
||||||
let onToggle: () -> Void
|
let onToggle: () -> Void
|
||||||
let onSelectDocument: (OutlineDocument) -> Void
|
let onSelectDocument: ([OutlineDocument]) -> Void
|
||||||
let onSearchInCollection: (OutlineCollection) -> Void
|
let onSearchInCollection: (OutlineCollection) -> Void
|
||||||
let onCollectionsChanged: () async -> Void
|
let onCollectionsChanged: () async -> Void
|
||||||
|
|
||||||
|
|||||||
@@ -7,14 +7,14 @@ 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> = []
|
||||||
let onSelectDocument: (OutlineCollection, OutlineDocument) -> Void
|
let onSelectDocument: (OutlineCollection, [OutlineDocument]) -> Void
|
||||||
let onSearchInCollection: (OutlineCollection) -> Void
|
let onSearchInCollection: (OutlineCollection) -> Void
|
||||||
|
|
||||||
init(
|
init(
|
||||||
apiClient: OutlineAPIClient,
|
apiClient: OutlineAPIClient,
|
||||||
selectedCollection: Binding<OutlineCollection?>,
|
selectedCollection: Binding<OutlineCollection?>,
|
||||||
selectedDocumentID: String?,
|
selectedDocumentID: String?,
|
||||||
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))
|
||||||
@@ -74,7 +74,7 @@ struct CollectionsTreeView: View {
|
|||||||
isSelected: selectedCollection?.id == collection.id,
|
isSelected: selectedCollection?.id == collection.id,
|
||||||
selectedDocumentID: selectedDocumentID,
|
selectedDocumentID: selectedDocumentID,
|
||||||
onToggle: { toggle(collection) },
|
onToggle: { toggle(collection) },
|
||||||
onSelectDocument: { document in onSelectDocument(collection, document) },
|
onSelectDocument: { chain in onSelectDocument(collection, chain) },
|
||||||
onSearchInCollection: onSearchInCollection,
|
onSearchInCollection: onSearchInCollection,
|
||||||
onCollectionsChanged: { await viewModel.load() }
|
onCollectionsChanged: { await viewModel.load() }
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import OutlineKit
|
|||||||
struct ContentView_macOS: View {
|
struct ContentView_macOS: View {
|
||||||
@Environment(SessionStore.self) private var session
|
@Environment(SessionStore.self) private var session
|
||||||
@State private var selectedCollection: OutlineCollection?
|
@State private var selectedCollection: OutlineCollection?
|
||||||
|
/// The real navigation stack, root to leaf — also the source of truth for
|
||||||
|
/// the toolbar breadcrumb, so the two can't drift out of sync.
|
||||||
@State private var documentPath: [OutlineDocument] = []
|
@State private var documentPath: [OutlineDocument] = []
|
||||||
@State private var globalSearchQuery = ""
|
@State private var globalSearchQuery = ""
|
||||||
@State private var contextualSearchQuery = ""
|
@State private var contextualSearchQuery = ""
|
||||||
@@ -26,19 +28,19 @@ struct ContentView_macOS: View {
|
|||||||
} detail: {
|
} detail: {
|
||||||
detail
|
detail
|
||||||
}
|
}
|
||||||
// Empty rather than `windowTitle`: with a real value, the native title
|
// Empty rather than a real value: with one, the native title rendered
|
||||||
// rendered in the same toolbar row as the custom `.navigation` pill
|
// in the same toolbar row as the custom `.navigation` pill below,
|
||||||
// below, visibly duplicating it. `.windowToolbarStyle(showsTitle:
|
// visibly duplicating it. `.windowToolbarStyle(showsTitle: false)` was
|
||||||
// false)` was tried to suppress just the native text, but it also
|
// tried to suppress just the native text, but it also suppressed the
|
||||||
// suppressed the custom `.navigation` item itself — an empty title
|
// custom `.navigation` item itself — an empty title sidesteps both
|
||||||
// sidesteps both problems (Window-menu entry is blank as a result).
|
// problems (Window-menu entry is blank as a result).
|
||||||
.navigationTitle("")
|
.navigationTitle("")
|
||||||
.toolbar {
|
.toolbar {
|
||||||
// Per HIG: "sidebar-toggle/back controls appear at the far leading
|
// Per HIG: "sidebar-toggle/back controls appear at the far leading
|
||||||
// edge, followed by the view title" — this is that title, not
|
// edge, followed by the view title" — this is that title, not
|
||||||
// centered. It doesn't collide with the back button because they're
|
// centered. It doesn't collide with the back button because they're
|
||||||
// mutually exclusive: whenever a document's pushed (back button
|
// mutually exclusive: whenever a document's pushed (back button
|
||||||
// visible), this shows the document's own title instead of falling
|
// visible), this shows the document hierarchy instead of falling
|
||||||
// back to the workspace badge.
|
// back to the workspace badge.
|
||||||
ToolbarItem(placement: .navigation) {
|
ToolbarItem(placement: .navigation) {
|
||||||
leadingToolbarContent
|
leadingToolbarContent
|
||||||
@@ -63,21 +65,29 @@ struct ContentView_macOS: View {
|
|||||||
Image(systemName: "magnifyingglass")
|
Image(systemName: "magnifyingglass")
|
||||||
Text("Search")
|
Text("Search")
|
||||||
}
|
}
|
||||||
} else if let currentDocument = documentPath.last, let selectedCollection {
|
} else if !documentPath.isEmpty, let selectedCollection {
|
||||||
// Back button (system-provided) → collection → document, so the
|
// Collection → every ancestor (icon only) → current document
|
||||||
// hierarchy reads left to right instead of just the leaf title.
|
// (icon + full title) — ancestors stay icon-only so a deep
|
||||||
|
// chain doesn't blow out the toolbar width.
|
||||||
HStack(spacing: 6) {
|
HStack(spacing: 6) {
|
||||||
CollectionRowView(collection: selectedCollection)
|
CollectionRowView(collection: selectedCollection)
|
||||||
Image(systemName: "chevron.right")
|
|
||||||
.font(.caption2.weight(.semibold))
|
ForEach(Array(documentPath.enumerated()), id: \.element.id) { index, document in
|
||||||
.foregroundStyle(.tertiary)
|
Image(systemName: "chevron.right")
|
||||||
if let emoji = currentDocument.emoji {
|
.font(.caption2.weight(.semibold))
|
||||||
Text(emoji)
|
.foregroundStyle(.tertiary)
|
||||||
} else {
|
|
||||||
Image(systemName: "doc.text")
|
if let emoji = document.emoji {
|
||||||
|
Text(emoji)
|
||||||
|
} else {
|
||||||
|
Image(systemName: "doc.text")
|
||||||
|
}
|
||||||
|
|
||||||
|
if index == documentPath.count - 1 {
|
||||||
|
Text(document.title.isEmpty ? "Untitled" : document.title)
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Text(currentDocument.title.isEmpty ? "Untitled" : currentDocument.title)
|
|
||||||
.lineLimit(1)
|
|
||||||
}
|
}
|
||||||
} else if let selectedCollection {
|
} else if let selectedCollection {
|
||||||
CollectionRowView(collection: selectedCollection)
|
CollectionRowView(collection: selectedCollection)
|
||||||
@@ -102,7 +112,7 @@ struct ContentView_macOS: View {
|
|||||||
apiClient: apiClient,
|
apiClient: apiClient,
|
||||||
selectedCollection: $selectedCollection,
|
selectedCollection: $selectedCollection,
|
||||||
selectedDocumentID: documentPath.last?.id,
|
selectedDocumentID: documentPath.last?.id,
|
||||||
onSelectDocument: selectDocument,
|
onSelectDocument: selectDocumentChain,
|
||||||
onSearchInCollection: searchInCollection
|
onSearchInCollection: searchInCollection
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@@ -118,16 +128,29 @@ struct ContentView_macOS: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `documentPath = [document]` (replacing a same-size array in one step)
|
/// Sidebar clicks (which know the real ancestor chain via DocumentNode):
|
||||||
/// unreliably updates `NavigationStack` on macOS — the stack can silently
|
/// replaces the whole stack with `chain`, so the toolbar shows full
|
||||||
/// keep showing the previous document. Popping to root and pushing again
|
/// hierarchy immediately rather than just the leaf.
|
||||||
/// as two separate state updates (so SwiftUI processes them as two render
|
private func selectDocumentChain(_ collection: OutlineCollection, _ chain: [OutlineDocument]) {
|
||||||
/// passes) is the reliable version of the same operation.
|
|
||||||
private func selectDocument(_ collection: OutlineCollection, _ document: OutlineDocument) {
|
|
||||||
selectedCollection = collection
|
selectedCollection = collection
|
||||||
|
replaceDocumentPath(with: chain)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Flat-list / search-result clicks: no known ancestors, single-level push.
|
||||||
|
private func openDocument(_ document: OutlineDocument) {
|
||||||
|
replaceDocumentPath(with: [document])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replacing `path` with a same-size array can silently fail to update
|
||||||
|
/// NavigationStack on macOS — popping to empty and pushing again as two
|
||||||
|
/// separate state updates (so SwiftUI processes them as two render
|
||||||
|
/// passes) is the reliable version of the same operation. Appending
|
||||||
|
/// (e.g. opening a sub-document from within the reader) doesn't hit this
|
||||||
|
/// and can mutate `documentPath` directly.
|
||||||
|
private func replaceDocumentPath(with chain: [OutlineDocument]) {
|
||||||
documentPath = []
|
documentPath = []
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
documentPath = [document]
|
documentPath = chain
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,12 +162,13 @@ struct ContentView_macOS: View {
|
|||||||
NavigationStack(path: $documentPath) {
|
NavigationStack(path: $documentPath) {
|
||||||
Group {
|
Group {
|
||||||
if !trimmedGlobalQuery.isEmpty {
|
if !trimmedGlobalQuery.isEmpty {
|
||||||
GlobalSearchResultsView(apiClient: apiClient, query: trimmedGlobalQuery)
|
GlobalSearchResultsView(apiClient: apiClient, query: trimmedGlobalQuery, onOpenDocument: openDocument)
|
||||||
} else if let selectedCollection {
|
} else if let selectedCollection {
|
||||||
CollectionOverviewView(
|
CollectionOverviewView(
|
||||||
apiClient: apiClient,
|
apiClient: apiClient,
|
||||||
collection: selectedCollection,
|
collection: selectedCollection,
|
||||||
searchQuery: $contextualSearchQuery
|
searchQuery: $contextualSearchQuery,
|
||||||
|
onOpenDocument: openDocument
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
ContentUnavailableView(
|
ContentUnavailableView(
|
||||||
@@ -155,7 +179,11 @@ struct ContentView_macOS: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationDestination(for: OutlineDocument.self) { document in
|
.navigationDestination(for: OutlineDocument.self) { document in
|
||||||
DocumentReaderView(apiClient: apiClient, document: document)
|
DocumentReaderView(
|
||||||
|
apiClient: apiClient,
|
||||||
|
document: document,
|
||||||
|
onOpenChild: { child in documentPath.append(child) }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.id(trimmedGlobalQuery.isEmpty ? (selectedCollection?.id ?? "none") : "search")
|
.id(trimmedGlobalQuery.isEmpty ? (selectedCollection?.id ?? "none") : "search")
|
||||||
|
|||||||
@@ -7,9 +7,13 @@ import OutlineKit
|
|||||||
/// isn't wired up yet, but link clicks and text selection still work.
|
/// isn't wired up yet, but link clicks and text selection still work.
|
||||||
struct DocumentReaderView: View {
|
struct DocumentReaderView: View {
|
||||||
@State private var viewModel: DocumentReaderViewModel
|
@State private var viewModel: DocumentReaderViewModel
|
||||||
|
/// Pushes a genuine new stack entry (not a reset+replace) — the back
|
||||||
|
/// button then correctly returns to this document, not the collection.
|
||||||
|
let onOpenChild: (OutlineDocument) -> Void
|
||||||
|
|
||||||
init(apiClient: OutlineAPIClient, document: OutlineDocument) {
|
init(apiClient: OutlineAPIClient, document: OutlineDocument, onOpenChild: @escaping (OutlineDocument) -> Void) {
|
||||||
_viewModel = State(initialValue: DocumentReaderViewModel(apiClient: apiClient, document: document))
|
_viewModel = State(initialValue: DocumentReaderViewModel(apiClient: apiClient, document: document))
|
||||||
|
self.onOpenChild = onOpenChild
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -36,6 +40,10 @@ struct DocumentReaderView: View {
|
|||||||
configuration: .init(heightBehavior: .fitsContent),
|
configuration: .init(heightBehavior: .fitsContent),
|
||||||
isEditable: false
|
isEditable: false
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if !viewModel.children.isEmpty {
|
||||||
|
childrenSection
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
@@ -43,5 +51,30 @@ struct DocumentReaderView: View {
|
|||||||
// No `.navigationTitle` here either — same reason as CollectionOverviewView.
|
// No `.navigationTitle` here either — same reason as CollectionOverviewView.
|
||||||
.task { await viewModel.loadFullContent() }
|
.task { await viewModel.loadFullContent() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var childrenSection: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
Divider()
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
|
||||||
|
Text("Sub-documents")
|
||||||
|
.font(.caption.weight(.semibold))
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
|
||||||
|
ForEach(viewModel.children) { child in
|
||||||
|
Button {
|
||||||
|
onOpenChild(child)
|
||||||
|
} label: {
|
||||||
|
DocumentRowView(document: child)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
|
||||||
|
if child.id != viewModel.children.last?.id {
|
||||||
|
Divider()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -8,11 +8,12 @@ final class DocumentReaderViewModel {
|
|||||||
var title: String
|
var title: String
|
||||||
var emoji: String?
|
var emoji: String?
|
||||||
var text: String
|
var text: String
|
||||||
|
var children: [OutlineDocument] = []
|
||||||
var isLoading = false
|
var isLoading = false
|
||||||
var errorMessage: String?
|
var errorMessage: String?
|
||||||
|
|
||||||
|
let documentId: String
|
||||||
private let apiClient: OutlineAPIClient
|
private let apiClient: OutlineAPIClient
|
||||||
private let documentId: String
|
|
||||||
|
|
||||||
init(apiClient: OutlineAPIClient, document: OutlineDocument) {
|
init(apiClient: OutlineAPIClient, document: OutlineDocument) {
|
||||||
self.apiClient = apiClient
|
self.apiClient = apiClient
|
||||||
@@ -37,5 +38,12 @@ final class DocumentReaderViewModel {
|
|||||||
} catch {
|
} catch {
|
||||||
errorMessage = "Couldn't load this document. Check your connection and try again."
|
errorMessage = "Couldn't load this document. Check your connection and try again."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
children = (try? await apiClient.listDocuments(
|
||||||
|
collectionId: nil,
|
||||||
|
parentDocumentId: documentId,
|
||||||
|
offset: 0,
|
||||||
|
limit: 100
|
||||||
|
)) ?? []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ final class DocumentsViewModel {
|
|||||||
defer { isLoading = false }
|
defer { isLoading = false }
|
||||||
|
|
||||||
do {
|
do {
|
||||||
documents = try await apiClient.listDocuments(collectionId: collection.id, offset: 0, limit: 100)
|
documents = try await apiClient.listDocuments(collectionId: collection.id, parentDocumentId: nil, offset: 0, limit: 100)
|
||||||
hasRemoteChanges = false
|
hasRemoteChanges = false
|
||||||
} catch {
|
} catch {
|
||||||
errorMessage = "Couldn't load documents. Check your connection and try again."
|
errorMessage = "Couldn't load documents. Check your connection and try again."
|
||||||
@@ -33,7 +33,7 @@ final class DocumentsViewModel {
|
|||||||
|
|
||||||
/// See CollectionsViewModel.checkForRemoteChanges — same reasoning.
|
/// See CollectionsViewModel.checkForRemoteChanges — same reasoning.
|
||||||
func checkForRemoteChanges() async {
|
func checkForRemoteChanges() async {
|
||||||
guard let fresh = try? await apiClient.listDocuments(collectionId: collection.id, offset: 0, limit: 100) else {
|
guard let fresh = try? await apiClient.listDocuments(collectionId: collection.id, parentDocumentId: nil, offset: 0, limit: 100) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if Self.fingerprint(fresh) != Self.fingerprint(documents) {
|
if Self.fingerprint(fresh) != Self.fingerprint(documents) {
|
||||||
|
|||||||
@@ -5,10 +5,12 @@ import OutlineKit
|
|||||||
struct GlobalSearchResultsView: View {
|
struct GlobalSearchResultsView: View {
|
||||||
@State private var viewModel: GlobalSearchViewModel
|
@State private var viewModel: GlobalSearchViewModel
|
||||||
let query: String
|
let query: String
|
||||||
|
let onOpenDocument: (OutlineDocument) -> Void
|
||||||
|
|
||||||
init(apiClient: OutlineAPIClient, query: String) {
|
init(apiClient: OutlineAPIClient, query: String, onOpenDocument: @escaping (OutlineDocument) -> Void) {
|
||||||
_viewModel = State(initialValue: GlobalSearchViewModel(apiClient: apiClient))
|
_viewModel = State(initialValue: GlobalSearchViewModel(apiClient: apiClient))
|
||||||
self.query = query
|
self.query = query
|
||||||
|
self.onOpenDocument = onOpenDocument
|
||||||
}
|
}
|
||||||
|
|
||||||
private var searchKey: String {
|
private var searchKey: String {
|
||||||
@@ -119,7 +121,9 @@ struct GlobalSearchResultsView: View {
|
|||||||
ContentUnavailableView.search(text: query)
|
ContentUnavailableView.search(text: query)
|
||||||
} else {
|
} else {
|
||||||
List(viewModel.results) { result in
|
List(viewModel.results) { result in
|
||||||
NavigationLink(value: result.document) {
|
Button {
|
||||||
|
onOpenDocument(result.document)
|
||||||
|
} label: {
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
DocumentRowView(document: result.document)
|
DocumentRowView(document: result.document)
|
||||||
Text(result.context)
|
Text(result.context)
|
||||||
@@ -129,6 +133,7 @@ struct GlobalSearchResultsView: View {
|
|||||||
}
|
}
|
||||||
.padding(.vertical, 2)
|
.padding(.vertical, 2)
|
||||||
}
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user