WIP: add collection rail create button

This commit is contained in:
Akshay Kolli
2026-06-30 01:34:05 -07:00
parent 95430e98cb
commit 800caebb83
2 changed files with 111 additions and 0 deletions

View File

@@ -54,6 +54,7 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
private let searchField = NSSearchField()
private let collectionScrollView = NSScrollView()
private let collectionStack = NSStackView()
private let addCollectionButton = NSButton()
private let itemsStack = NSStackView()
private let scrollView = NSScrollView()
private let statusLabel = NSTextField(labelWithString: "")
@@ -71,6 +72,9 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
private var defersVisualReloads = false
private var pendingItemReload = false
private var pendingCollectionReload = false
#if DEBUG
private var collectionNameProviderForTesting: (() -> String?)?
#endif
init(viewModel: ClipboardPanelViewModel, onClose: @escaping () -> Void, onSettings: @escaping () -> Void = {}) {
self.viewModel = viewModel
@@ -154,6 +158,7 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
collectionScrollView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
collectionScrollView.setContentHuggingPriority(.defaultLow, for: .horizontal)
collectionScrollView.heightAnchor.constraint(equalToConstant: 30).isActive = true
configureAddCollectionButton()
configureCollectionButtons()
let settingsButton = iconButton("gearshape", toolTip: "Settings", action: #selector(openSettings))
@@ -373,9 +378,33 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
customCollectionButtons[collectionName] = chip
collectionStack.addArrangedSubview(chip)
}
collectionStack.addArrangedSubview(addCollectionButton)
updateAddCollectionButtonState()
sizeCollectionDocument()
}
private func configureAddCollectionButton() {
let image = NSImage(systemSymbolName: "plus", accessibilityDescription: "New collection")
image?.isTemplate = true
addCollectionButton.image = image
addCollectionButton.imagePosition = .imageOnly
addCollectionButton.imageScaling = .scaleProportionallyDown
addCollectionButton.isBordered = false
addCollectionButton.wantsLayer = true
addCollectionButton.layer?.cornerRadius = 13
addCollectionButton.layer?.borderWidth = 0.6
addCollectionButton.layer?.borderColor = NSColor.separatorColor.withAlphaComponent(0.16).cgColor
addCollectionButton.layer?.backgroundColor = NSColor.windowBackgroundColor.withAlphaComponent(0.26).cgColor
addCollectionButton.contentTintColor = .secondaryLabelColor
addCollectionButton.toolTip = "Add selected clip to a new collection"
addCollectionButton.setAccessibilityLabel("Add selected clip to a new collection")
addCollectionButton.target = self
addCollectionButton.action = #selector(addSelectedClipToCollection)
addCollectionButton.translatesAutoresizingMaskIntoConstraints = false
addCollectionButton.widthAnchor.constraint(equalToConstant: 30).isActive = true
addCollectionButton.heightAnchor.constraint(equalToConstant: 26).isActive = true
}
private func collectionTitle(for mode: ClipboardSortMode) -> String {
switch mode {
case .mostRecent: return "Clipboard"
@@ -578,6 +607,13 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
if let selectedCard {
scrollCardIntoView(selectedCard)
}
updateAddCollectionButtonState()
}
private func updateAddCollectionButtonState() {
let hasSelectedItem = viewModel.selectedItem != nil
addCollectionButton.isEnabled = hasSelectedItem
addCollectionButton.alphaValue = hasSelectedItem ? 1.0 : 0.42
}
private func scrollCardIntoView(_ card: NSView) {
@@ -949,6 +985,22 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
emptyStateText
}
var debugAddCollectionButtonIsEnabled: Bool {
addCollectionButton.isEnabled
}
var debugCollectionRailContainsAddButton: Bool {
collectionStack.arrangedSubviews.contains(addCollectionButton)
}
func debugSetCollectionNameProvider(_ provider: @escaping () -> String?) {
collectionNameProviderForTesting = provider
}
func debugPressAddCollectionButton() {
addSelectedClipToCollection()
}
#endif
func controlTextDidChange(_ notification: Notification) {
@@ -997,6 +1049,39 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
@objc private func openSettings() {
onSettings()
}
@objc private func addSelectedClipToCollection() {
guard viewModel.selectedItem != nil,
let name = requestCollectionName() else {
return
}
viewModel.assignSelected(to: name)
}
private func requestCollectionName() -> String? {
#if DEBUG
if let collectionNameProviderForTesting {
return ClipboardCollectionDefaults.normalizedName(collectionNameProviderForTesting())
}
#endif
let input = NSTextField(frame: NSRect(x: 0, y: 0, width: 260, height: 24))
input.placeholderString = "Collection name"
input.stringValue = ""
let alert = NSAlert()
alert.messageText = "New Collection"
alert.informativeText = "Name this collection and add the selected clip to it."
alert.accessoryView = input
alert.addButton(withTitle: "Add")
alert.addButton(withTitle: "Cancel")
alert.window.initialFirstResponder = input
guard alert.runModal() == .alertFirstButtonReturn else {
return nil
}
return ClipboardCollectionDefaults.normalizedName(input.stringValue)
}
}
private final class CollectionChipView: NSView {

View File

@@ -141,6 +141,32 @@ final class ClipboardPanelViewTests: XCTestCase {
XCTAssertEqual(fixture.view.debugSelectedCollectionTitle, "Links")
}
func testCollectionRailAddButtonCreatesCollectionForSelectedClip() {
let fixture = makePanelFixture()
XCTAssertTrue(fixture.view.debugCollectionRailContainsAddButton)
XCTAssertFalse(fixture.view.debugAddCollectionButtonIsEnabled)
fixture.store.upsert(makeTextItem("Collect this note", store: fixture.store))
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertTrue(fixture.view.debugAddCollectionButtonIsEnabled)
fixture.view.debugSetCollectionNameProvider { " Research Stack " }
fixture.view.debugPressAddCollectionButton()
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertEqual(fixture.viewModel.statusMessage, "Added to Research Stack")
XCTAssertEqual(fixture.view.debugCustomCollectionTitles, ["Research Stack"])
fixture.viewModel.selectCollection(named: "Research Stack")
drainMainQueue()
XCTAssertEqual(fixture.viewModel.visibleItems.map(\.payload), ["Collect this note"])
}
func testSelectedCardActionsRespectSelectedKind() {
let fixture = makePanelFixture()
fixture.store.upsert(makeTextItem("Plain text", store: fixture.store))