App-side chrome: new "Pointer Cursor" toggle (Settings -> Editor, outpost.pointerCursorEnabled, on by default) driving a shared .pointerCursorOnHover() view modifier (push/pop NSCursor.pointingHand, macOS-only, no-op on iOS) wired onto every clickable sidebar/list row: collection tree rows + disclosure chevron, flat collection list, document outline rows, collection-overview tab bar + document/search rows, global search results, command palette rows. In-document link hover: MarkdownEditorConfiguration gets a new pointerCursorOverLinksWhileEditing flag (default true). Read-only mode already showed a pointing hand over links unconditionally; editable mode never did (I-beam only) until now - applyReadOnlyCursor in NativeTextView+CursorRects.swift now applies the same over any .link range while editing too, gated by the flag. Wiki links already carry .link alongside their own custom .wikiLinkID, so they're covered with no extra work. Closes out the last item from the original preferences-wiring scope.
189 lines
6.1 KiB
Swift
189 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)
|
|
.pointerCursorOnHover()
|
|
.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
|