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.
188 lines
6.1 KiB
Swift
188 lines
6.1 KiB
Swift
#if os(macOS)
|
|
import SwiftUI
|
|
import OutlineKit
|
|
|
|
@MainActor
|
|
struct CollectionTreeRow: View {
|
|
@Environment(StarStore.self) private var starStore
|
|
|
|
let apiClient: OutlineAPIClient
|
|
let collection: OutlineCollection
|
|
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
|
|
let onCollectionsChanged: () async -> Void
|
|
|
|
@State private var sortOption: SidebarSortOption = .manual
|
|
@State private var documentsRefreshToken = 0
|
|
|
|
@State private var isShowingRenameAlert = false
|
|
@State private var renameText = ""
|
|
@State private var isShowingDeleteConfirmation = false
|
|
@State private var isShowingNewDocumentSheet = false
|
|
@State private var actionErrorMessage: String?
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 0) {
|
|
Button(action: onToggle) {
|
|
HStack(spacing: 6) {
|
|
Image(systemName: "chevron.right")
|
|
.font(.caption2.weight(.semibold))
|
|
.foregroundStyle(.secondary)
|
|
.rotationEffect(.degrees(isExpanded ? 90 : 0))
|
|
.frame(width: 12)
|
|
|
|
CollectionRowView(collection: collection)
|
|
|
|
Spacer(minLength: 0)
|
|
|
|
if starStore.isStarred(collectionId: collection.id) {
|
|
Image(systemName: "star.fill")
|
|
.font(.caption2)
|
|
.foregroundStyle(.yellow)
|
|
}
|
|
}
|
|
.padding(.vertical, 4)
|
|
.padding(.horizontal, 6)
|
|
.contentShape(Rectangle())
|
|
.background(
|
|
isSelected ? Color.accentColor.opacity(0.15) : Color.clear,
|
|
in: RoundedRectangle(cornerRadius: 6)
|
|
)
|
|
}
|
|
.buttonStyle(.plain)
|
|
.animation(.easeInOut(duration: 0.15), value: isExpanded)
|
|
.contextMenu { contextMenuContent }
|
|
|
|
if isExpanded {
|
|
CollectionDocumentsOutline(
|
|
apiClient: apiClient,
|
|
collection: collection,
|
|
sortOption: sortOption,
|
|
refreshToken: documentsRefreshToken,
|
|
externalRefreshToken: externalRefreshToken,
|
|
selectedDocumentID: selectedDocumentID,
|
|
onSelectDocument: onSelectDocument
|
|
)
|
|
.padding(.leading, 18)
|
|
}
|
|
}
|
|
.alert("Rename Collection", isPresented: $isShowingRenameAlert) {
|
|
TextField("Name", text: $renameText)
|
|
Button("Cancel", role: .cancel) {}
|
|
Button("Rename") {
|
|
Task { await rename() }
|
|
}
|
|
}
|
|
.confirmationDialog(
|
|
"Delete \"\(collection.name)\"?",
|
|
isPresented: $isShowingDeleteConfirmation,
|
|
titleVisibility: .visible
|
|
) {
|
|
Button("Delete", role: .destructive) {
|
|
Task { await delete() }
|
|
}
|
|
Button("Cancel", role: .cancel) {}
|
|
} message: {
|
|
Text("This deletes the collection and all of its documents. This can't be undone.")
|
|
}
|
|
.alert("Couldn't Complete Action", isPresented: .constant(actionErrorMessage != nil)) {
|
|
Button("OK") { actionErrorMessage = nil }
|
|
} message: {
|
|
Text(actionErrorMessage ?? "")
|
|
}
|
|
.sheet(isPresented: $isShowingNewDocumentSheet) {
|
|
NewDocumentSheet(apiClient: apiClient, initialCollectionID: collection.id) { _ in
|
|
documentsRefreshToken += 1
|
|
Task { await onCollectionsChanged() }
|
|
}
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private var contextMenuContent: some View {
|
|
Button(starStore.isStarred(collectionId: collection.id) ? "Unstar" : "Star") {
|
|
Task { await star() }
|
|
}
|
|
|
|
Divider()
|
|
|
|
Button("New Document") {
|
|
isShowingNewDocumentSheet = true
|
|
}
|
|
|
|
Divider()
|
|
|
|
Button("Rename…") {
|
|
renameText = collection.name
|
|
isShowingRenameAlert = true
|
|
}
|
|
|
|
Divider()
|
|
|
|
Menu("Sort in Sidebar") {
|
|
Picker("Sort", selection: $sortOption) {
|
|
ForEach(SidebarSortOption.allCases) { option in
|
|
Text(option.label).tag(option)
|
|
}
|
|
}
|
|
}
|
|
|
|
Divider()
|
|
|
|
Button("Export…") {
|
|
Task { await export() }
|
|
}
|
|
|
|
Button("Search in Collection") {
|
|
onSearchInCollection(collection)
|
|
}
|
|
|
|
Divider()
|
|
|
|
Button("Delete…", role: .destructive) {
|
|
isShowingDeleteConfirmation = true
|
|
}
|
|
}
|
|
|
|
private func star() async {
|
|
do {
|
|
try await starStore.toggleCollection(collection.id, apiClient: apiClient)
|
|
} catch {
|
|
actionErrorMessage = outlineErrorMessage(error, fallback: "Couldn't update the star on this collection.")
|
|
}
|
|
}
|
|
|
|
private func rename() async {
|
|
do {
|
|
_ = try await apiClient.updateCollection(UpdateCollectionRequest(id: collection.id, name: renameText))
|
|
await onCollectionsChanged()
|
|
} catch {
|
|
actionErrorMessage = outlineErrorMessage(error, fallback: "Couldn't rename this collection.")
|
|
}
|
|
}
|
|
|
|
private func export() async {
|
|
do {
|
|
_ = try await apiClient.exportCollection(ExportCollectionRequest(id: collection.id))
|
|
} catch {
|
|
actionErrorMessage = outlineErrorMessage(error, fallback: "Couldn't start the export.")
|
|
}
|
|
}
|
|
|
|
private func delete() async {
|
|
do {
|
|
try await apiClient.deleteCollection(id: collection.id)
|
|
await onCollectionsChanged()
|
|
} catch {
|
|
actionErrorMessage = outlineErrorMessage(error, fallback: "Couldn't delete this collection.")
|
|
}
|
|
}
|
|
}
|
|
#endif
|