Merge branch 'main' into feature/home-page
# Conflicts: # Outpost/Features/Collections/CollectionDocumentsOutline.swift # Outpost/Features/Collections/CollectionsTreeView.swift # Outpost/Features/Collections/ContentView_macOS.swift # Outpost/Features/Collections/DocumentReaderView.swift
This commit is contained in:
@@ -299,15 +299,15 @@ private struct DuplicateDocumentResponse: Decodable {
|
||||
let documents: [OutlineDocument]
|
||||
}
|
||||
|
||||
private struct ListStarsResponse: Decodable {
|
||||
let stars: [OutlineStar]
|
||||
}
|
||||
|
||||
private struct PinsListPayload: Decodable {
|
||||
let pins: [OutlinePin]
|
||||
let documents: [OutlineDocument]
|
||||
}
|
||||
|
||||
private struct ListStarsResponse: Decodable {
|
||||
let stars: [OutlineStar]
|
||||
}
|
||||
|
||||
private struct StarIDParams: Encodable {
|
||||
let id: String
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import OutlineKit
|
||||
/// client-side rather than `collections.documents`.
|
||||
struct CollectionDocumentsOutline: View {
|
||||
let apiClient: OutlineAPIClient
|
||||
let collection: OutlineCollection
|
||||
@State private var viewModel: DocumentsViewModel
|
||||
let sortOption: SidebarSortOption
|
||||
let refreshToken: Int
|
||||
@@ -24,6 +25,11 @@ struct CollectionDocumentsOutline: View {
|
||||
/// toolbar render the real hierarchy instead of just the leaf title.
|
||||
let onSelectDocument: ([OutlineDocument]) -> Void
|
||||
|
||||
/// Loaded once per collection (`pins.list` is collection-scoped) rather
|
||||
/// than per-row — a per-row `pins.list`/lookup would be an N+1 call for
|
||||
/// every document in the tree.
|
||||
@State private var pinsByDocumentID: [String: OutlinePin] = [:]
|
||||
|
||||
private var tree: [DocumentNode] {
|
||||
buildDocumentTree(from: viewModel.documents, sortedBy: sortOption)
|
||||
}
|
||||
@@ -38,6 +44,7 @@ struct CollectionDocumentsOutline: View {
|
||||
onSelectDocument: @escaping ([OutlineDocument]) -> Void
|
||||
) {
|
||||
self.apiClient = apiClient
|
||||
self.collection = collection
|
||||
_viewModel = State(initialValue: DocumentsViewModel(apiClient: apiClient, collection: collection))
|
||||
self.sortOption = sortOption
|
||||
self.refreshToken = refreshToken
|
||||
@@ -65,8 +72,10 @@ struct CollectionDocumentsOutline: View {
|
||||
depth: 0,
|
||||
ancestors: [],
|
||||
selectedDocumentID: selectedDocumentID,
|
||||
pinsByDocumentID: pinsByDocumentID,
|
||||
onSelectDocument: onSelectDocument,
|
||||
onDocumentsChanged: { await viewModel.load() }
|
||||
onDocumentsChanged: { await viewModel.load() },
|
||||
onPinsChanged: { await loadPins() }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -74,7 +83,15 @@ struct CollectionDocumentsOutline: View {
|
||||
// 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() }
|
||||
.task(id: "\(refreshToken)-\(externalRefreshToken)") {
|
||||
await viewModel.load()
|
||||
await loadPins()
|
||||
}
|
||||
}
|
||||
|
||||
private func loadPins() async {
|
||||
guard let pins = try? await apiClient.listPins(ListPinsRequest(collectionId: collection.id)) else { return }
|
||||
pinsByDocumentID = Dictionary(uniqueKeysWithValues: pins.map { ($0.documentId, $0) })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,8 +110,10 @@ private struct DocumentNodeRow: View {
|
||||
/// Chain from root down to (not including) this node.
|
||||
let ancestors: [OutlineDocument]
|
||||
let selectedDocumentID: String?
|
||||
let pinsByDocumentID: [String: OutlinePin]
|
||||
let onSelectDocument: ([OutlineDocument]) -> Void
|
||||
let onDocumentsChanged: () async -> Void
|
||||
let onPinsChanged: () async -> Void
|
||||
|
||||
@State private var isExpanded = false
|
||||
|
||||
@@ -185,8 +204,10 @@ private struct DocumentNodeRow: View {
|
||||
depth: depth + 1,
|
||||
ancestors: ancestors + [node.document],
|
||||
selectedDocumentID: selectedDocumentID,
|
||||
pinsByDocumentID: pinsByDocumentID,
|
||||
onSelectDocument: onSelectDocument,
|
||||
onDocumentsChanged: onDocumentsChanged
|
||||
onDocumentsChanged: onDocumentsChanged,
|
||||
onPinsChanged: onPinsChanged
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -277,7 +298,11 @@ private struct DocumentNodeRow: View {
|
||||
Button(starStore.isStarred(documentId: node.document.id) ? "Unstar" : "Star") {
|
||||
Task { await star() }
|
||||
}
|
||||
// No `subscriptions.*` endpoint in the API — nothing to back this with.
|
||||
// subscriptions.* does exist and works (confirmed against a live
|
||||
// server via the reader's menu) — not shown here because
|
||||
// subscriptions.list is per-document, so reflecting accurate
|
||||
// per-row state for every document in the tree would mean an N+1
|
||||
// call storm. Use the reader's ⋯ menu instead.
|
||||
Button("Unsubscribe") {}
|
||||
.disabled(true)
|
||||
|
||||
@@ -323,12 +348,11 @@ private struct DocumentNodeRow: View {
|
||||
Button("New Document") {
|
||||
isShowingNewDocumentSheet = true
|
||||
}
|
||||
// `pins.*` is real and works (see `OutlinePin`), but "Pin to
|
||||
// Collection" (non-nil `collectionId`) is distinct from the reader
|
||||
// toolbar's "Pin to Home" and needs its own per-row state/N+1
|
||||
// consideration for a whole tree — not wired here yet.
|
||||
Button("Pin") {}
|
||||
.disabled(true)
|
||||
// Scoped to this collection (`collection.id`) — "Pin to Collection",
|
||||
// distinct from the reader toolbar's "Pin to Home" (collectionId: nil).
|
||||
Button(pinsByDocumentID[node.document.id] != nil ? "Unpin from Collection" : "Pin to Collection") {
|
||||
Task { await togglePin() }
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
@@ -372,6 +396,19 @@ private struct DocumentNodeRow: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func togglePin() async {
|
||||
do {
|
||||
if let pin = pinsByDocumentID[node.document.id] {
|
||||
try await apiClient.deletePin(id: pin.id)
|
||||
} else {
|
||||
_ = try await apiClient.createPin(CreatePinRequest(documentId: node.document.id, collectionId: node.document.collectionId))
|
||||
}
|
||||
await onPinsChanged()
|
||||
} catch {
|
||||
actionErrorMessage = outlineErrorMessage(error, fallback: "Couldn't update pin state.")
|
||||
}
|
||||
}
|
||||
|
||||
private func rename() async {
|
||||
do {
|
||||
_ = try await apiClient.updateDocument(UpdateDocumentRequest(id: node.document.id, title: renameText))
|
||||
|
||||
Reference in New Issue
Block a user