From 800caebb8318081bcce43f056b1c133b9e2fcea7 Mon Sep 17 00:00:00 2001 From: Akshay Kolli Date: Tue, 30 Jun 2026 01:34:05 -0700 Subject: [PATCH] WIP: add collection rail create button --- .../clipbored/views/ClipboardPanelView.swift | 85 +++++++++++++++++++ .../ClipboardPanelViewTests.swift | 26 ++++++ 2 files changed, 111 insertions(+) diff --git a/sources/clipbored/views/ClipboardPanelView.swift b/sources/clipbored/views/ClipboardPanelView.swift index fa6396a..079b091 100644 --- a/sources/clipbored/views/ClipboardPanelView.swift +++ b/sources/clipbored/views/ClipboardPanelView.swift @@ -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 { diff --git a/tests/clipboredtests/ClipboardPanelViewTests.swift b/tests/clipboredtests/ClipboardPanelViewTests.swift index 1b4edc6..575b8f8 100644 --- a/tests/clipboredtests/ClipboardPanelViewTests.swift +++ b/tests/clipboredtests/ClipboardPanelViewTests.swift @@ -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))