feat(collections): show real document hierarchy in sidebar outline
Builds a tree from parentDocumentId over the flat documents.list response instead of a flat list — collections.documents returns this shape server-side already, but its NavigationNode has no emoji or timestamps, which would break per-row icons and date-based sidebar sort. Each node gets its own expand/collapse now.
This commit is contained in:
@@ -2,17 +2,17 @@
|
||||
import SwiftUI
|
||||
import OutlineKit
|
||||
|
||||
/// Flat document list nested under an expanded collection in the sidebar tree.
|
||||
/// Documents can themselves have children (`parentDocumentId`) in Outline — not
|
||||
/// represented here yet, this is a first pass at the collection-level expansion.
|
||||
/// Nested document outline under an expanded collection in the sidebar tree —
|
||||
/// real hierarchy via `parentDocumentId`, see DocumentNode for why that's
|
||||
/// client-side rather than `collections.documents`.
|
||||
struct CollectionDocumentsOutline: View {
|
||||
@State private var viewModel: DocumentsViewModel
|
||||
let sortOption: SidebarSortOption
|
||||
let refreshToken: Int
|
||||
let onSelectDocument: (OutlineDocument) -> Void
|
||||
|
||||
private var sortedDocuments: [OutlineDocument] {
|
||||
sortOption.sorted(viewModel.documents)
|
||||
private var tree: [DocumentNode] {
|
||||
buildDocumentTree(from: viewModel.documents, sortedBy: sortOption)
|
||||
}
|
||||
|
||||
init(
|
||||
@@ -40,33 +40,71 @@ struct CollectionDocumentsOutline: View {
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.vertical, 6)
|
||||
} else {
|
||||
ForEach(sortedDocuments) { document in
|
||||
Button {
|
||||
onSelectDocument(document)
|
||||
} label: {
|
||||
HStack(spacing: 8) {
|
||||
if let emoji = document.emoji {
|
||||
Text(emoji)
|
||||
.font(.body)
|
||||
} else {
|
||||
Image(systemName: "doc.text")
|
||||
.font(.callout)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
Text(document.title.isEmpty ? "Untitled" : document.title)
|
||||
.font(.body)
|
||||
.lineLimit(1)
|
||||
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.padding(.vertical, 6)
|
||||
ForEach(tree) { node in
|
||||
DocumentNodeRow(node: node, depth: 0, onSelectDocument: onSelectDocument)
|
||||
}
|
||||
}
|
||||
}
|
||||
.task(id: refreshToken) { await viewModel.load() }
|
||||
}
|
||||
}
|
||||
|
||||
private struct DocumentNodeRow: View {
|
||||
let node: DocumentNode
|
||||
let depth: Int
|
||||
let onSelectDocument: (OutlineDocument) -> Void
|
||||
|
||||
@State private var isExpanded = false
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
HStack(spacing: 6) {
|
||||
if node.children.isEmpty {
|
||||
Color.clear.frame(width: 12)
|
||||
} else {
|
||||
Button {
|
||||
isExpanded.toggle()
|
||||
} label: {
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.caption2.weight(.semibold))
|
||||
.foregroundStyle(.secondary)
|
||||
.rotationEffect(.degrees(isExpanded ? 90 : 0))
|
||||
.frame(width: 12)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
Button {
|
||||
onSelectDocument(node.document)
|
||||
} label: {
|
||||
HStack(spacing: 8) {
|
||||
if let emoji = node.document.emoji {
|
||||
Text(emoji)
|
||||
.font(.body)
|
||||
} else {
|
||||
Image(systemName: "doc.text")
|
||||
.font(.callout)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
Text(node.document.title.isEmpty ? "Untitled" : node.document.title)
|
||||
.font(.body)
|
||||
.lineLimit(1)
|
||||
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
.padding(.vertical, 6)
|
||||
.padding(.leading, CGFloat(depth) * 16)
|
||||
|
||||
if isExpanded {
|
||||
ForEach(node.children) { child in
|
||||
DocumentNodeRow(node: child, depth: depth + 1, onSelectDocument: onSelectDocument)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import Foundation
|
||||
import OutlineKit
|
||||
|
||||
/// Reconstructs the real document hierarchy client-side from `parentDocumentId`.
|
||||
/// `documents.list` already returns the full flat set for a collection
|
||||
/// (including children, not just roots), so no separate tree-shaped endpoint
|
||||
/// is needed — `collections.documents` exists and returns exactly this shape,
|
||||
/// but its `NavigationNode` carries no emoji/timestamps, which would break
|
||||
/// per-row icons and date-based sidebar sorting.
|
||||
struct DocumentNode: Identifiable {
|
||||
let document: OutlineDocument
|
||||
let children: [DocumentNode]
|
||||
var id: String { document.id }
|
||||
}
|
||||
|
||||
func buildDocumentTree(from documents: [OutlineDocument], sortedBy sort: SidebarSortOption) -> [DocumentNode] {
|
||||
let byParent = Dictionary(grouping: documents, by: { $0.parentDocumentId })
|
||||
|
||||
func makeNodes(parentId: String?) -> [DocumentNode] {
|
||||
sort.sorted(byParent[parentId] ?? []).map { document in
|
||||
DocumentNode(document: document, children: makeNodes(parentId: document.id))
|
||||
}
|
||||
}
|
||||
|
||||
return makeNodes(parentId: nil)
|
||||
}
|
||||
Reference in New Issue
Block a user