WIP: add clipboard stack workflow

This commit is contained in:
Akshay Kolli
2026-06-30 02:11:50 -07:00
parent 475e387f7f
commit 7f41127431
6 changed files with 262 additions and 9 deletions

View File

@@ -18,8 +18,10 @@ enum ClipboardPanelShortcutAction: Equatable {
case copyPlainText case copyPlainText
case open case open
case pastePlainText case pastePlainText
case pasteStackNext
case preview case preview
case reveal case reveal
case toggleStack
} }
final class ClipboardPanelController: NSObject, NSWindowDelegate, QLPreviewPanelDataSource, QLPreviewPanelDelegate { final class ClipboardPanelController: NSObject, NSWindowDelegate, QLPreviewPanelDataSource, QLPreviewPanelDelegate {
@@ -378,10 +380,14 @@ final class ClipboardPanelController: NSObject, NSWindowDelegate, QLPreviewPanel
viewModel.openSelected() viewModel.openSelected()
case .pastePlainText: case .pastePlainText:
viewModel.pasteSelectedPlainText() viewModel.pasteSelectedPlainText()
case .pasteStackNext:
viewModel.pasteNextStackItem()
case .preview: case .preview:
previewSelected() previewSelected()
case .reveal: case .reveal:
viewModel.revealSelected() viewModel.revealSelected()
case .toggleStack:
viewModel.toggleSelectedStackMembership()
} }
} }
@@ -445,10 +451,14 @@ final class ClipboardPanelController: NSObject, NSWindowDelegate, QLPreviewPanel
let relevantModifiers = modifiers.intersection(.deviceIndependentFlagsMask) let relevantModifiers = modifiers.intersection(.deviceIndependentFlagsMask)
guard relevantModifiers == [.command, .shift] else { return nil } guard relevantModifiers == [.command, .shift] else { return nil }
switch keyCode { switch keyCode {
case 1:
return .toggleStack
case 8: case 8:
return .copyPlainText return .copyPlainText
case 9: case 9:
return .pastePlainText return .pastePlainText
case 36:
return .pasteStackNext
default: default:
return nil return nil
} }

View File

@@ -323,6 +323,11 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
viewModel.onCollectionsChanged = { [weak self] in viewModel.onCollectionsChanged = { [weak self] in
self?.handleCollectionsChanged() self?.handleCollectionsChanged()
} }
viewModel.onStackChanged = { [weak self] in
self?.reloadItems()
self?.updateSelection()
self?.updateStatus(self?.viewModel.statusMessage ?? "")
}
viewModel.onCaptureStatusChanged = { [weak self] in viewModel.onCaptureStatusChanged = { [weak self] in
self?.updateStatus(self?.viewModel.statusMessage ?? "") self?.updateStatus(self?.viewModel.statusMessage ?? "")
} }
@@ -515,7 +520,9 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
item: item, item: item,
thumbnail: viewModel.thumbnail(for: item), thumbnail: viewModel.thumbnail(for: item),
index: index, index: index,
collectionNames: collectionNames collectionNames: collectionNames,
isStacked: viewModel.isItemStacked(at: index),
stackCount: viewModel.stackCount
) )
card.onSelect = { [weak self] selected in card.onSelect = { [weak self] selected in
self?.viewModel.selectItem(at: selected) self?.viewModel.selectItem(at: selected)
@@ -536,6 +543,19 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
self?.viewModel.selectItem(at: selected) self?.viewModel.selectItem(at: selected)
self?.viewModel.copySelectedPlainText() self?.viewModel.copySelectedPlainText()
} }
card.onToggleStack = { [weak self] selected in
self?.viewModel.selectItem(at: selected)
self?.viewModel.toggleSelectedStackMembership()
}
card.onPasteStackNext = { [weak self] in
self?.viewModel.pasteNextStackItem()
}
card.onCopyStackNext = { [weak self] in
self?.viewModel.copyNextStackItem()
}
card.onClearStack = { [weak self] in
self?.viewModel.clearStack()
}
card.onEditText = { [weak self] selected in card.onEditText = { [weak self] selected in
self?.editText(at: selected) self?.editText(at: selected)
} }
@@ -677,7 +697,7 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
if lower.hasPrefix("captured") || lower.contains("capture running") || lower.contains("capture is running") || lower.contains("capture resumed") { if lower.hasPrefix("captured") || lower.contains("capture running") || lower.contains("capture is running") || lower.contains("capture resumed") {
return .ready return .ready
} }
if lower.hasPrefix("copied") || lower.hasPrefix("pasted") || lower.hasPrefix("updated") { if lower.hasPrefix("copied") || lower.hasPrefix("pasted") || lower.hasPrefix("updated") || lower.hasPrefix("added") || lower.hasPrefix("removed") || lower.hasPrefix("cleared") {
return .action return .action
} }
if lower.hasPrefix("error") || lower.contains("failed") { if lower.hasPrefix("error") || lower.contains("failed") {
@@ -1318,6 +1338,10 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
var onCopy: (Int) -> Void = { _ in } var onCopy: (Int) -> Void = { _ in }
var onPastePlainText: (Int) -> Void = { _ in } var onPastePlainText: (Int) -> Void = { _ in }
var onCopyPlainText: (Int) -> Void = { _ in } var onCopyPlainText: (Int) -> Void = { _ in }
var onToggleStack: (Int) -> Void = { _ in }
var onPasteStackNext: () -> Void = {}
var onCopyStackNext: () -> Void = {}
var onClearStack: () -> Void = {}
var onEditText: (Int) -> Void = { _ in } var onEditText: (Int) -> Void = { _ in }
var onPreview: (Int) -> Void = { _ in } var onPreview: (Int) -> Void = { _ in }
var onPasteboardWriters: (Int) -> [NSPasteboardWriting] = { _ in [] } var onPasteboardWriters: (Int) -> [NSPasteboardWriting] = { _ in [] }
@@ -1330,6 +1354,8 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
private let index: Int private let index: Int
private let itemKind: ClipboardItemKind private let itemKind: ClipboardItemKind
private let itemIsPinned: Bool private let itemIsPinned: Bool
private let itemIsStacked: Bool
private let stackCount: Int
private let itemCollectionName: String? private let itemCollectionName: String?
private let collectionNames: [String] private let collectionNames: [String]
private let contentView = NSView() private let contentView = NSView()
@@ -1343,10 +1369,19 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
private var mouseDownLocation: NSPoint? private var mouseDownLocation: NSPoint?
private var trackingAreaRef: NSTrackingArea? private var trackingAreaRef: NSTrackingArea?
init(item: ClipboardItem, thumbnail: NSImage?, index: Int, collectionNames: [String] = []) { init(
item: ClipboardItem,
thumbnail: NSImage?,
index: Int,
collectionNames: [String] = [],
isStacked: Bool = false,
stackCount: Int = 0
) {
self.index = index self.index = index
self.itemKind = item.kind self.itemKind = item.kind
self.itemIsPinned = item.isPinned self.itemIsPinned = item.isPinned
self.itemIsStacked = isStacked
self.stackCount = stackCount
self.itemCollectionName = ClipboardCollectionDefaults.normalizedName(item.collectionName) self.itemCollectionName = ClipboardCollectionDefaults.normalizedName(item.collectionName)
self.collectionNames = collectionNames.compactMap { ClipboardCollectionDefaults.normalizedName($0) } self.collectionNames = collectionNames.compactMap { ClipboardCollectionDefaults.normalizedName($0) }
super.init(frame: .zero) super.init(frame: .zero)
@@ -1505,6 +1540,12 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
addMenuItem("Paste Plain Text", action: #selector(pastePlainTextFromMenu), to: menu) addMenuItem("Paste Plain Text", action: #selector(pastePlainTextFromMenu), to: menu)
addMenuItem("Copy Plain Text", action: #selector(copyPlainTextFromMenu), to: menu) addMenuItem("Copy Plain Text", action: #selector(copyPlainTextFromMenu), to: menu)
} }
addMenuItem(itemIsStacked ? "Remove from Stack" : "Add to Stack", action: #selector(toggleStackFromMenu), to: menu)
if stackCount > 0 {
addMenuItem("Paste Stack Next", action: #selector(pasteStackNextFromMenu), to: menu)
addMenuItem("Copy Stack Next", action: #selector(copyStackNextFromMenu), to: menu)
addMenuItem("Clear Stack", action: #selector(clearStackFromMenu), to: menu)
}
if canEditText { if canEditText {
addMenuItem("Edit", action: #selector(editTextFromMenu), to: menu) addMenuItem("Edit", action: #selector(editTextFromMenu), to: menu)
} }
@@ -1641,6 +1682,7 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
cardActionButton("doc.on.doc", toolTip: "Copy", action: #selector(copyFromMenu)), cardActionButton("doc.on.doc", toolTip: "Copy", action: #selector(copyFromMenu)),
cardActionButton(itemIsPinned ? "pin.slash" : "pin", toolTip: pinTitle, action: #selector(togglePinFromMenu)) cardActionButton(itemIsPinned ? "pin.slash" : "pin", toolTip: pinTitle, action: #selector(togglePinFromMenu))
] ]
actionRailButtons.append(cardActionButton("square.stack.3d.up", toolTip: itemIsStacked ? "Remove from Stack" : "Add to Stack", action: #selector(toggleStackFromMenu)))
if canEditText { if canEditText {
actionRailButtons.append(cardActionButton("pencil", toolTip: "Edit", action: #selector(editTextFromMenu))) actionRailButtons.append(cardActionButton("pencil", toolTip: "Edit", action: #selector(editTextFromMenu)))
} }
@@ -1725,6 +1767,22 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
onCopyPlainText(index) onCopyPlainText(index)
} }
@objc private func toggleStackFromMenu() {
onToggleStack(index)
}
@objc private func pasteStackNextFromMenu() {
onPasteStackNext()
}
@objc private func copyStackNextFromMenu() {
onCopyStackNext()
}
@objc private func clearStackFromMenu() {
onClearStack()
}
@objc private func editTextFromMenu() { @objc private func editTextFromMenu() {
onEditText(index) onEditText(index)
} }

View File

@@ -44,6 +44,12 @@ final class ClipboardPanelViewModel {
private let cacheService: ClipboardCacheService private let cacheService: ClipboardCacheService
private let pasteService: PasteActionService private let pasteService: PasteActionService
private var selectedItemID: UUID? private var selectedItemID: UUID?
private var stackItemIDs: [UUID] = [] {
didSet {
guard oldValue != stackItemIDs else { return }
notifyMain { self.onStackChanged?() }
}
}
var targetApplicationProvider: () -> NSRunningApplication? = { nil } var targetApplicationProvider: () -> NSRunningApplication? = { nil }
var willPasteToTarget: () -> Void = {} var willPasteToTarget: () -> Void = {}
var onVisibleItemsChanged: (([ClipboardItem]) -> Void)? var onVisibleItemsChanged: (([ClipboardItem]) -> Void)?
@@ -51,6 +57,7 @@ final class ClipboardPanelViewModel {
var onStatusMessageChanged: ((String) -> Void)? var onStatusMessageChanged: ((String) -> Void)?
var onSortModeChanged: ((ClipboardSortMode) -> Void)? var onSortModeChanged: ((ClipboardSortMode) -> Void)?
var onCollectionsChanged: (() -> Void)? var onCollectionsChanged: (() -> Void)?
var onStackChanged: (() -> Void)?
var onCaptureStatusChanged: (() -> Void)? var onCaptureStatusChanged: (() -> Void)?
init(store: ClipboardStore, settings: SettingsModel, cacheService: ClipboardCacheService) { init(store: ClipboardStore, settings: SettingsModel, cacheService: ClipboardCacheService) {
@@ -85,6 +92,10 @@ final class ClipboardPanelViewModel {
items.count items.count
} }
var stackCount: Int {
stackItemIDs.count
}
var collectionNames: [String] { var collectionNames: [String] {
let assignedNames = Set( let assignedNames = Set(
items.compactMap { item -> String? in items.compactMap { item -> String? in
@@ -188,6 +199,55 @@ final class ClipboardPanelViewModel {
settings.setPasteStatus(message: result.message) settings.setPasteStatus(message: result.message)
} }
func isItemStacked(at index: Int) -> Bool {
guard index >= 0 && index < visibleItems.count else { return false }
return stackItemIDs.contains(visibleItems[index].id)
}
func toggleSelectedStackMembership() {
guard let item = selectedItem else { return }
if let existingIndex = stackItemIDs.firstIndex(of: item.id) {
stackItemIDs.remove(at: existingIndex)
statusMessage = "Removed from Stack"
return
}
stackItemIDs.append(item.id)
statusMessage = "Added to Stack"
}
func clearStack() {
guard !stackItemIDs.isEmpty else {
statusMessage = "Stack is empty"
return
}
stackItemIDs.removeAll()
statusMessage = "Cleared Stack"
}
func copyNextStackItem() {
guard let item = nextStackItem() else {
statusMessage = "Stack is empty"
return
}
let result = pasteService.copy(item)
handleStackActionResult(result, item: item)
}
func pasteNextStackItem() {
guard let item = nextStackItem() else {
statusMessage = "Stack is empty"
return
}
let result = pasteService.paste(item, targetApp: targetApplicationProvider())
if case .pasted = result {
willPasteToTarget()
}
handleStackActionResult(result, item: item)
}
func pasteboardWriters(forItemAt index: Int) -> [NSPasteboardWriting] { func pasteboardWriters(forItemAt index: Int) -> [NSPasteboardWriting] {
guard index >= 0 && index < visibleItems.count else { return [] } guard index >= 0 && index < visibleItems.count else { return [] }
return pasteService.pasteboardWriters(for: visibleItems[index]) return pasteService.pasteboardWriters(for: visibleItems[index])
@@ -311,6 +371,7 @@ final class ClipboardPanelViewModel {
} }
func recomputeVisibleItems() { func recomputeVisibleItems() {
pruneStackItems()
let previousSelection = selectedItemID let previousSelection = selectedItemID
let query = searchText.clipboardTrimmed.lowercased() let query = searchText.clipboardTrimmed.lowercased()
visibleItems = computeVisibleItems(from: items, query: query, sortMode: sortMode, collectionName: selectedCollectionName) visibleItems = computeVisibleItems(from: items, query: query, sortMode: sortMode, collectionName: selectedCollectionName)
@@ -331,6 +392,48 @@ final class ClipboardPanelViewModel {
onCollectionsChanged?() onCollectionsChanged?()
} }
private func nextStackItem() -> ClipboardItem? {
pruneStackItems()
guard let id = stackItemIDs.first else { return nil }
return items.first { $0.id == id }
}
private func handleStackActionResult(_ result: PasteActionService.PasteActionResult, item: ClipboardItem) {
if case .failed(let message) = result {
statusMessage = message
return
}
consumeStackItem(item.id)
store.markUsed(item.id)
selectedItemID = item.id
switch result {
case .copiedNeedsPermission:
statusMessage = "Copied from Stack. Grant Accessibility access to paste automatically."
case .pasted:
statusMessage = "Pasted from Stack"
case .copied:
statusMessage = "Copied from Stack"
default:
statusMessage = result.message
}
settings.setPasteStatus(message: statusMessage)
}
private func consumeStackItem(_ id: UUID) {
guard let index = stackItemIDs.firstIndex(of: id) else { return }
stackItemIDs.remove(at: index)
}
private func pruneStackItems() {
guard !stackItemIDs.isEmpty else { return }
let existingIDs = Set(items.map(\.id))
let pruned = stackItemIDs.filter { existingIDs.contains($0) }
if pruned != stackItemIDs {
stackItemIDs = pruned
}
}
internal func computeVisibleItems( internal func computeVisibleItems(
from items: [ClipboardItem], from items: [ClipboardItem],
query: String, query: String,

View File

@@ -159,8 +159,10 @@ final class ClipboardPanelControllerTests: XCTestCase {
} }
func testModifiedShortcutsMapToPlainTextActions() { func testModifiedShortcutsMapToPlainTextActions() {
XCTAssertEqual(ClipboardPanelController.modifiedShortcutAction(forKeyCode: 1, modifiers: [.command, .shift]), .toggleStack)
XCTAssertEqual(ClipboardPanelController.modifiedShortcutAction(forKeyCode: 8, modifiers: [.command, .shift]), .copyPlainText) XCTAssertEqual(ClipboardPanelController.modifiedShortcutAction(forKeyCode: 8, modifiers: [.command, .shift]), .copyPlainText)
XCTAssertEqual(ClipboardPanelController.modifiedShortcutAction(forKeyCode: 9, modifiers: [.command, .shift]), .pastePlainText) XCTAssertEqual(ClipboardPanelController.modifiedShortcutAction(forKeyCode: 9, modifiers: [.command, .shift]), .pastePlainText)
XCTAssertEqual(ClipboardPanelController.modifiedShortcutAction(forKeyCode: 36, modifiers: [.command, .shift]), .pasteStackNext)
} }
func testModifiedShortcutsRequireCommandShiftOnly() { func testModifiedShortcutsRequireCommandShiftOnly() {

View File

@@ -304,6 +304,69 @@ final class ClipboardPanelViewModelTests: XCTestCase {
XCTAssertEqual(store.items.first?.useCount, 1) XCTAssertEqual(store.items.first?.useCount, 1)
} }
func testStackPastesQueuedItemsInOrderAndConsumesThem() {
let settings = makeSettings()
let cacheService = makeCacheService()
let store = makeStore(settings: settings, cacheService: cacheService)
let first = makeTextItem("first stacked clip", createdAt: Date(timeIntervalSince1970: 100))
let second = makeTextItem("second stacked clip", createdAt: Date(timeIntervalSince1970: 200))
store.upsert(first)
store.upsert(second)
store.flushPersistenceForTesting()
let viewModel = ClipboardPanelViewModel(store: store, settings: settings, cacheService: cacheService)
waitForVisibleItems(in: viewModel, count: 2)
viewModel.selectItem(at: 1)
viewModel.toggleSelectedStackMembership()
viewModel.selectItem(at: 0)
viewModel.toggleSelectedStackMembership()
XCTAssertEqual(viewModel.stackCount, 2)
XCTAssertEqual(viewModel.statusMessage, "Added to Stack")
viewModel.pasteNextStackItem()
store.flushPersistenceForTesting()
XCTAssertEqual(NSPasteboard.general.string(forType: .string), "first stacked clip")
XCTAssertEqual(viewModel.statusMessage, "Copied from Stack")
XCTAssertEqual(viewModel.stackCount, 1)
XCTAssertEqual(store.items.first(where: { $0.id == first.id })?.useCount, 1)
viewModel.copyNextStackItem()
store.flushPersistenceForTesting()
XCTAssertEqual(NSPasteboard.general.string(forType: .string), "second stacked clip")
XCTAssertEqual(viewModel.statusMessage, "Copied from Stack")
XCTAssertEqual(viewModel.stackCount, 0)
}
func testStackToggleAndClearUpdateCount() {
let settings = makeSettings()
let cacheService = makeCacheService()
let store = makeStore(settings: settings, cacheService: cacheService)
store.upsert(makeTextItem("stack toggle clip", createdAt: Date(timeIntervalSince1970: 100)))
store.flushPersistenceForTesting()
let viewModel = ClipboardPanelViewModel(store: store, settings: settings, cacheService: cacheService)
waitForVisibleItems(in: viewModel, count: 1)
viewModel.toggleSelectedStackMembership()
XCTAssertEqual(viewModel.stackCount, 1)
XCTAssertEqual(viewModel.statusMessage, "Added to Stack")
XCTAssertTrue(viewModel.isItemStacked(at: 0))
viewModel.toggleSelectedStackMembership()
XCTAssertEqual(viewModel.stackCount, 0)
XCTAssertEqual(viewModel.statusMessage, "Removed from Stack")
XCTAssertFalse(viewModel.isItemStacked(at: 0))
viewModel.toggleSelectedStackMembership()
viewModel.clearStack()
XCTAssertEqual(viewModel.stackCount, 0)
XCTAssertEqual(viewModel.statusMessage, "Cleared Stack")
}
func testUpdateSelectedTextRefreshesVisibleItemAndSearch() { func testUpdateSelectedTextRefreshesVisibleItemAndSearch() {
let settings = makeSettings() let settings = makeSettings()
let cacheService = makeCacheService() let cacheService = makeCacheService()

View File

@@ -184,8 +184,8 @@ final class ClipboardPanelViewTests: XCTestCase {
fixture.store.upsert(makeTextItem("Plain text", store: fixture.store)) fixture.store.upsert(makeTextItem("Plain text", store: fixture.store))
drainMainQueue() drainMainQueue()
XCTAssertEqual(fixture.view.debugFirstCardVisibleActionLabels, ["Paste", "Copy", "Pin", "Edit", "Delete"]) XCTAssertEqual(fixture.view.debugFirstCardVisibleActionLabels, ["Paste", "Copy", "Pin", "Add to Stack", "Edit", "Delete"])
XCTAssertEqual(fixture.view.debugFirstCardVisibleActionRailWidth, 154) XCTAssertEqual(fixture.view.debugFirstCardVisibleActionRailWidth, 182)
XCTAssertFalse(fixture.view.debugFirstCardFooterDetailIsHidden) XCTAssertFalse(fixture.view.debugFirstCardFooterDetailIsHidden)
XCTAssertTrue(fixture.view.debugFirstCardHeaderBadgeIsHidden) XCTAssertTrue(fixture.view.debugFirstCardHeaderBadgeIsHidden)
@@ -194,8 +194,8 @@ final class ClipboardPanelViewTests: XCTestCase {
fixture.viewModel.selectFirstItem() fixture.viewModel.selectFirstItem()
XCTAssertEqual(fixture.viewModel.visibleItems.first?.kind, .file) XCTAssertEqual(fixture.viewModel.visibleItems.first?.kind, .file)
XCTAssertEqual(fixture.view.debugFirstCardVisibleActionLabels, ["Paste", "Copy", "Pin", "Preview", "Open", "Reveal", "Delete"]) XCTAssertEqual(fixture.view.debugFirstCardVisibleActionLabels, ["Paste", "Copy", "Pin", "Add to Stack", "Preview", "Open", "Reveal", "Delete"])
XCTAssertEqual(fixture.view.debugFirstCardVisibleActionRailWidth, 210) XCTAssertEqual(fixture.view.debugFirstCardVisibleActionRailWidth, 238)
XCTAssertFalse(fixture.view.debugFirstCardFooterDetailIsHidden) XCTAssertFalse(fixture.view.debugFirstCardFooterDetailIsHidden)
XCTAssertTrue(fixture.view.debugFirstCardHeaderBadgeIsHidden) XCTAssertTrue(fixture.view.debugFirstCardHeaderBadgeIsHidden)
} }
@@ -333,7 +333,7 @@ final class ClipboardPanelViewTests: XCTestCase {
XCTAssertEqual( XCTAssertEqual(
fixture.view.debugFirstCardMenuTitles, fixture.view.debugFirstCardMenuTitles,
["Paste", "Copy", "Edit", "Pin", "Add to Collection", "-", "Open", "Reveal in Finder", "-", "Delete"] ["Paste", "Copy", "Add to Stack", "Edit", "Pin", "Add to Collection", "-", "Open", "Reveal in Finder", "-", "Delete"]
) )
XCTAssertEqual( XCTAssertEqual(
fixture.view.debugFirstCardCollectionMenuTitles, fixture.view.debugFirstCardCollectionMenuTitles,
@@ -349,10 +349,27 @@ final class ClipboardPanelViewTests: XCTestCase {
XCTAssertEqual( XCTAssertEqual(
fixture.view.debugFirstCardMenuTitles, fixture.view.debugFirstCardMenuTitles,
["Paste", "Copy", "Paste Plain Text", "Copy Plain Text", "Quick Look", "Pin", "Add to Collection", "-", "Open", "Reveal in Finder", "-", "Delete"] ["Paste", "Copy", "Paste Plain Text", "Copy Plain Text", "Add to Stack", "Quick Look", "Pin", "Add to Collection", "-", "Open", "Reveal in Finder", "-", "Delete"]
) )
} }
func testStackedCardsExposeStackManagementActions() {
let fixture = makePanelFixture()
fixture.store.upsert(makeTextItem("Stackable text", store: fixture.store))
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
fixture.viewModel.toggleSelectedStackMembership()
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertEqual(
fixture.view.debugFirstCardMenuTitles,
["Paste", "Copy", "Remove from Stack", "Paste Stack Next", "Copy Stack Next", "Clear Stack", "Edit", "Pin", "Add to Collection", "-", "Open", "Reveal in Finder", "-", "Delete"]
)
XCTAssertEqual(fixture.view.debugFirstCardVisibleActionLabels, ["Paste", "Copy", "Pin", "Remove from Stack", "Edit", "Delete"])
}
func testCollectionMenuOffersExistingCustomCollections() { func testCollectionMenuOffersExistingCustomCollections() {
let fixture = makePanelFixture() let fixture = makePanelFixture()
var existing = makeTextItem("Existing client note", store: fixture.store) var existing = makeTextItem("Existing client note", store: fixture.store)