Tighten browser import sheet UI

This commit is contained in:
Lawrence Chen 2026-03-17 01:51:57 -07:00
parent aac8a41ba2
commit ffcd3fdfaa
No known key found for this signature in database
4 changed files with 202 additions and 89 deletions

View file

@ -4740,13 +4740,13 @@
"en": {
"stringUnit": {
"state": "translated",
"value": "Bookmarks, settings, and extensions import are not available yet."
"value": "Bookmarks, settings, and extensions are not available yet."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "ブックマーク、設定、拡張機能のインポートにはまだ対応していません。"
"value": "ブックマーク、設定、拡張機能はまだ利用できません。"
}
}
}
@ -5029,13 +5029,13 @@
"en": {
"stringUnit": {
"state": "translated",
"value": "cmux destination"
"value": "Destination"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "cmux の保存先"
"value": "保存先"
}
}
}
@ -5080,13 +5080,13 @@
"en": {
"stringUnit": {
"state": "translated",
"value": "Imported cookies and history go into the selected cmux browser profile."
"value": "Imported data goes into the selected cmux profile."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "インポートしたCookieと履歴は、選択したcmuxブラウザープロファイルに保存されます。"
"value": "インポートしたデータは、選択した cmux プロファイルに保存されます。"
}
}
}
@ -5097,13 +5097,13 @@
"en": {
"stringUnit": {
"state": "translated",
"value": "All selected source profiles will be merged into the chosen cmux browser profile."
"value": "All selected source profiles go into one cmux profile."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "選択した元プロファイルはすべて、選んだ cmux ブラウザープロファイルにまとめて取り込まれます。"
"value": "選択した元プロファイルは、1つの cmux プロファイルにまとめて取り込まれます。"
}
}
}
@ -5114,13 +5114,13 @@
"en": {
"stringUnit": {
"state": "translated",
"value": "Missing cmux profiles are created when import starts."
"value": "Missing cmux profiles are created on import."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "不足している cmux プロファイルは、インポート開始時に作成されます。"
"value": "不足している cmux プロファイルは、インポート時に作成されます。"
}
}
}
@ -5131,13 +5131,13 @@
"en": {
"stringUnit": {
"state": "translated",
"value": "Merge all into one cmux profile"
"value": "Merge into one"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "すべてを1つの cmux プロファイルにまとめる"
"value": "1つにまとめる"
}
}
}
@ -5148,13 +5148,13 @@
"en": {
"stringUnit": {
"state": "translated",
"value": "Keep profiles separate"
"value": "Separate profiles"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "プロファイルを分けたまま取り込む"
"value": "分けて取り込む"
}
}
}
@ -5233,13 +5233,13 @@
"en": {
"stringUnit": {
"state": "translated",
"value": "Limit to"
"value": "Domains"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "対象ドメイン"
"value": "ドメイン"
}
}
}
@ -5250,13 +5250,13 @@
"en": {
"stringUnit": {
"state": "translated",
"value": "Optional domains only (e.g. github.com, openai.com)"
"value": "Optional domains, comma-separated"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "任意のドメインのみ(例: github.com, openai.com"
"value": "任意のドメインをカンマ区切りで指定"
}
}
}
@ -5505,13 +5505,13 @@
"en": {
"stringUnit": {
"state": "translated",
"value": "Source"
"value": "Browser"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "インポート元"
"value": "ブラウザー"
}
}
}
@ -5539,13 +5539,13 @@
"en": {
"stringUnit": {
"state": "translated",
"value": "Source Profiles"
"value": "Profiles"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "プロファイル"
"value": "プロファイル"
}
}
}
@ -5556,13 +5556,13 @@
"en": {
"stringUnit": {
"state": "translated",
"value": "Choose one or more source profiles. Step 3 lets you keep them separate or merge them into one cmux profile."
"value": "Select one or more profiles."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "元プロファイルを1つ以上選択してください。3 / 3 で、分けたまま取り込むか、1つの cmux プロファイルにまとめるかを選べます。"
"value": "1つ以上のプロファイルを選択してください。"
}
}
}
@ -5607,13 +5607,13 @@
"en": {
"stringUnit": {
"state": "translated",
"value": "Step 3 of 3: Choose what to import from %@ and where to put it."
"value": "Step 3 of 3"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "3 / 3: %@ から何をインポートし、どこに保存するかを選択します。"
"value": "3 / 3"
}
}
}
@ -5624,13 +5624,13 @@
"en": {
"stringUnit": {
"state": "translated",
"value": "Step 1 of 3: Choose the browser to import from."
"value": "Step 1 of 3"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "1 / 3: インポート元のブラウザーを選択します。"
"value": "1 / 3"
}
}
}
@ -5641,13 +5641,13 @@
"en": {
"stringUnit": {
"state": "translated",
"value": "Step 2 of 3: Choose source profiles from %@."
"value": "Step 2 of 3"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "2 / 3: %@ の元プロファイルを選択します。"
"value": "2 / 3"
}
}
}

View file

@ -6837,6 +6837,18 @@ struct BrowserImportStep3Presentation: Equatable {
}
}
struct BrowserImportSourceProfilesPresentation: Equatable {
let scrollHeight: CGFloat
let showsHelpText: Bool
init(profileCount: Int) {
let visibleRows = min(max(profileCount, 1), 5)
let contentHeight = CGFloat(visibleRows * 26 + 14)
scrollHeight = max(76, contentHeight)
showsHelpText = profileCount > 1
}
}
enum BrowserImportPlanResolver {
@MainActor
static func defaultPlan(
@ -8378,6 +8390,7 @@ final class BrowserDataImportCoordinator {
private let sourceProfilesEmptyLabel = NSTextField(wrappingLabelWithString: "")
private let sourceProfilesHelpLabel = NSTextField(labelWithString: "")
private let sourceProfilesScrollView = NSScrollView()
private var sourceProfilesScrollHeightConstraint: NSLayoutConstraint?
private let dataTypesContainer = NSStackView()
private let validationLabel = NSTextField(labelWithString: "")
private let destinationModeContainer = NSStackView()
@ -8387,6 +8400,7 @@ final class BrowserDataImportCoordinator {
private let mergeDestinationRow = NSStackView()
private let mergeDestinationPopup = NSPopUpButton(frame: .zero, pullsDown: false)
private let destinationHelpLabel = NSTextField(wrappingLabelWithString: "")
private let additionalDataNoteLabel = NSTextField(wrappingLabelWithString: "")
private let cookiesCheckbox = NSButton(checkboxWithTitle: "", target: nil, action: nil)
private let historyCheckbox = NSButton(checkboxWithTitle: "", target: nil, action: nil)
@ -8412,7 +8426,7 @@ final class BrowserDataImportCoordinator {
?? fallbackDestinationProfileID
self.mergeDestinationProfileID = self.initialDestinationProfileID
self.panel = NSPanel(
contentRect: NSRect(x: 0, y: 0, width: 620, height: 420),
contentRect: NSRect(x: 0, y: 0, width: 560, height: 292),
styleMask: [.titled, .closable],
backing: .buffered,
defer: false
@ -8538,6 +8552,7 @@ final class BrowserDataImportCoordinator {
guard selectedSourceProfiles.count > 1 else { return }
destinationMode = sender == separateProfilesRadio ? .separateProfiles : .mergeIntoOne
rebuildStep3DestinationUI()
updatePanelSize()
}
@objc
@ -8560,6 +8575,13 @@ final class BrowserDataImportCoordinator {
validationLabel.isHidden = true
}
@objc
private func handleImportOptionChanged(_ sender: NSButton) {
validationLabel.isHidden = true
updateAdditionalDataNoteVisibility()
updatePanelSize()
}
private func setupUI() {
panel.title = String(
localized: "browser.import.title",
@ -8570,7 +8592,7 @@ final class BrowserDataImportCoordinator {
panel.standardWindowButton(.miniaturizeButton)?.isHidden = true
panel.standardWindowButton(.zoomButton)?.isHidden = true
let contentView = NSView(frame: NSRect(x: 0, y: 0, width: 620, height: 420))
let contentView = NSView(frame: NSRect(x: 0, y: 0, width: 560, height: 292))
contentView.translatesAutoresizingMaskIntoConstraints = false
panel.contentView = contentView
@ -8580,9 +8602,9 @@ final class BrowserDataImportCoordinator {
defaultValue: "Import Browser Data"
)
)
titleLabel.font = NSFont.systemFont(ofSize: 24, weight: .semibold)
titleLabel.font = NSFont.systemFont(ofSize: 22, weight: .semibold)
stepLabel.font = NSFont.systemFont(ofSize: 15, weight: .medium)
stepLabel.font = NSFont.systemFont(ofSize: 13, weight: .semibold)
stepLabel.textColor = .secondaryLabelColor
setupSourceContainer()
@ -8594,6 +8616,7 @@ final class BrowserDataImportCoordinator {
validationLabel.isHidden = true
validationLabel.lineBreakMode = .byWordWrapping
validationLabel.maximumNumberOfLines = 3
validationLabel.translatesAutoresizingMaskIntoConstraints = false
backButton.target = self
backButton.action = #selector(handleBack)
@ -8631,23 +8654,32 @@ final class BrowserDataImportCoordinator {
validationLabel,
])
contentStack.orientation = .vertical
contentStack.spacing = 10
contentStack.spacing = 8
contentStack.alignment = .leading
contentStack.translatesAutoresizingMaskIntoConstraints = false
sourceContainer.translatesAutoresizingMaskIntoConstraints = false
sourceProfilesContainer.translatesAutoresizingMaskIntoConstraints = false
dataTypesContainer.translatesAutoresizingMaskIntoConstraints = false
guard let panelContent = panel.contentView else { return }
panelContent.addSubview(contentStack)
panelContent.addSubview(buttonRow)
NSLayoutConstraint.activate([
contentStack.topAnchor.constraint(equalTo: panelContent.topAnchor, constant: 18),
contentStack.leadingAnchor.constraint(equalTo: panelContent.leadingAnchor, constant: 20),
contentStack.trailingAnchor.constraint(equalTo: panelContent.trailingAnchor, constant: -20),
contentStack.topAnchor.constraint(equalTo: panelContent.topAnchor, constant: 16),
contentStack.leadingAnchor.constraint(equalTo: panelContent.leadingAnchor, constant: 18),
contentStack.trailingAnchor.constraint(equalTo: panelContent.trailingAnchor, constant: -18),
buttonRow.topAnchor.constraint(greaterThanOrEqualTo: contentStack.bottomAnchor, constant: 14),
buttonRow.leadingAnchor.constraint(equalTo: panelContent.leadingAnchor, constant: 20),
buttonRow.trailingAnchor.constraint(equalTo: panelContent.trailingAnchor, constant: -20),
buttonRow.bottomAnchor.constraint(equalTo: panelContent.bottomAnchor, constant: -16),
buttonRow.leadingAnchor.constraint(equalTo: panelContent.leadingAnchor, constant: 18),
buttonRow.trailingAnchor.constraint(equalTo: panelContent.trailingAnchor, constant: -18),
buttonRow.bottomAnchor.constraint(equalTo: panelContent.bottomAnchor, constant: -14),
sourceContainer.widthAnchor.constraint(equalTo: contentStack.widthAnchor),
sourceProfilesContainer.widthAnchor.constraint(equalTo: contentStack.widthAnchor),
dataTypesContainer.widthAnchor.constraint(equalTo: contentStack.widthAnchor),
validationLabel.widthAnchor.constraint(equalTo: contentStack.widthAnchor),
])
}
@ -8663,23 +8695,27 @@ final class BrowserDataImportCoordinator {
labelWithString: String(localized: "browser.import.source", defaultValue: "Source")
)
sourceLabel.alignment = .right
sourceLabel.frame.size.width = 80
sourceLabel.frame.size.width = 64
sourcePopup.setContentHuggingPriority(.defaultLow, for: .horizontal)
sourcePopup.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
let sourceRow = NSStackView(views: [sourceLabel, sourcePopup])
sourceRow.orientation = .horizontal
sourceRow.spacing = 8
sourceRow.alignment = .centerY
sourceRow.distribution = .fill
let detectedLabel = NSTextField(
wrappingLabelWithString: InstalledBrowserDetector.summaryText(for: browsers)
)
detectedLabel.font = NSFont.systemFont(ofSize: 12)
detectedLabel.font = NSFont.systemFont(ofSize: 11)
detectedLabel.textColor = .secondaryLabelColor
detectedLabel.maximumNumberOfLines = 2
detectedLabel.preferredMaxLayoutWidth = 500
sourceContainer.orientation = .vertical
sourceContainer.spacing = 10
sourceContainer.spacing = 8
sourceContainer.alignment = .leading
sourceContainer.addArrangedSubview(sourceRow)
sourceContainer.addArrangedSubview(detectedLabel)
@ -8692,17 +8728,17 @@ final class BrowserDataImportCoordinator {
defaultValue: "Source Profiles"
)
)
sourceProfilesTitle.font = NSFont.systemFont(ofSize: 13, weight: .semibold)
sourceProfilesTitle.font = NSFont.systemFont(ofSize: 12, weight: .semibold)
sourceProfilesList.orientation = .vertical
sourceProfilesList.spacing = 6
sourceProfilesList.alignment = .leading
sourceProfilesList.translatesAutoresizingMaskIntoConstraints = false
sourceProfilesEmptyLabel.font = NSFont.systemFont(ofSize: 13)
sourceProfilesEmptyLabel.font = NSFont.systemFont(ofSize: 12)
sourceProfilesEmptyLabel.textColor = .secondaryLabelColor
sourceProfilesEmptyLabel.maximumNumberOfLines = 0
sourceProfilesEmptyLabel.preferredMaxLayoutWidth = 520
sourceProfilesEmptyLabel.preferredMaxLayoutWidth = 500
sourceProfilesDocumentView.frame = NSRect(x: 0, y: 0, width: 1, height: 1)
sourceProfilesDocumentView.translatesAutoresizingMaskIntoConstraints = false
@ -8721,19 +8757,22 @@ final class BrowserDataImportCoordinator {
sourceProfilesScrollView.documentView = sourceProfilesDocumentView
sourceProfilesScrollView.translatesAutoresizingMaskIntoConstraints = false
sourceProfilesScrollView.contentView.postsBoundsChangedNotifications = true
sourceProfilesScrollView.heightAnchor.constraint(equalToConstant: 180).isActive = true
sourceProfilesScrollHeightConstraint = sourceProfilesScrollView.heightAnchor.constraint(equalToConstant: 76)
sourceProfilesScrollHeightConstraint?.isActive = true
sourceProfilesScrollView.widthAnchor.constraint(equalTo: sourceProfilesContainer.widthAnchor).isActive = true
sourceProfilesHelpLabel.font = NSFont.systemFont(ofSize: 12)
sourceProfilesHelpLabel.font = NSFont.systemFont(ofSize: 11)
sourceProfilesHelpLabel.textColor = .secondaryLabelColor
sourceProfilesHelpLabel.maximumNumberOfLines = 2
sourceProfilesHelpLabel.lineBreakMode = .byWordWrapping
sourceProfilesHelpLabel.preferredMaxLayoutWidth = 500
sourceProfilesHelpLabel.stringValue = String(
localized: "browser.import.sourceProfiles.help",
defaultValue: "Choose one or more source profiles. Step 3 lets you keep them separate or merge them into one cmux profile."
)
sourceProfilesContainer.orientation = .vertical
sourceProfilesContainer.spacing = 10
sourceProfilesContainer.spacing = 8
sourceProfilesContainer.alignment = .leading
sourceProfilesContainer.addArrangedSubview(sourceProfilesTitle)
sourceProfilesContainer.addArrangedSubview(sourceProfilesScrollView)
@ -8758,6 +8797,12 @@ final class BrowserDataImportCoordinator {
localized: "browser.import.additionalData",
defaultValue: "Additional data (bookmarks, settings, extensions)"
)
cookiesCheckbox.target = self
cookiesCheckbox.action = #selector(handleImportOptionChanged(_:))
historyCheckbox.target = self
historyCheckbox.action = #selector(handleImportOptionChanged(_:))
additionalDataCheckbox.target = self
additionalDataCheckbox.action = #selector(handleImportOptionChanged(_:))
cookiesCheckbox.setAccessibilityIdentifier("BrowserImportCookiesCheckbox")
historyCheckbox.setAccessibilityIdentifier("BrowserImportHistoryCheckbox")
additionalDataCheckbox.setAccessibilityIdentifier("BrowserImportAdditionalDataCheckbox")
@ -8782,25 +8827,29 @@ final class BrowserDataImportCoordinator {
mergeDestinationPopup.target = self
mergeDestinationPopup.action = #selector(handleMergeDestinationChanged(_:))
mergeDestinationPopup.setContentHuggingPriority(.defaultLow, for: .horizontal)
mergeDestinationPopup.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
separateDestinationRows.orientation = .vertical
separateDestinationRows.spacing = 8
separateDestinationRows.spacing = 6
separateDestinationRows.alignment = .leading
mergeDestinationRow.orientation = .horizontal
mergeDestinationRow.spacing = 8
mergeDestinationRow.spacing = 6
mergeDestinationRow.alignment = .centerY
destinationHelpLabel.font = NSFont.systemFont(ofSize: 12)
destinationHelpLabel.font = NSFont.systemFont(ofSize: 11)
destinationHelpLabel.textColor = .secondaryLabelColor
destinationHelpLabel.maximumNumberOfLines = 3
destinationHelpLabel.preferredMaxLayoutWidth = 540
destinationHelpLabel.maximumNumberOfLines = 2
destinationHelpLabel.preferredMaxLayoutWidth = 500
domainField.placeholderString = String(
localized: "browser.import.domain.placeholder",
defaultValue: "Optional domains only (e.g. github.com, openai.com)"
)
domainField.stringValue = ""
domainField.setContentHuggingPriority(.defaultLow, for: .horizontal)
domainField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
let destinationTitleLabel = NSTextField(
labelWithString: String(
@ -8808,32 +8857,32 @@ final class BrowserDataImportCoordinator {
defaultValue: "cmux destination"
)
)
destinationTitleLabel.font = NSFont.systemFont(ofSize: 13, weight: .semibold)
destinationTitleLabel.font = NSFont.systemFont(ofSize: 12, weight: .semibold)
let domainLabel = NSTextField(
labelWithString: String(localized: "browser.import.domain", defaultValue: "Limit to")
)
domainLabel.alignment = .right
domainLabel.frame.size.width = 80
domainLabel.frame.size.width = 72
let domainRow = NSStackView(views: [domainLabel, domainField])
domainRow.orientation = .horizontal
domainRow.spacing = 8
domainRow.alignment = .centerY
domainRow.distribution = .fill
let noteLabel = NSTextField(
wrappingLabelWithString: String(
localized: "browser.import.additionalData.note",
defaultValue: "Bookmarks, settings, and extensions import are not available yet."
)
additionalDataNoteLabel.stringValue = String(
localized: "browser.import.additionalData.note",
defaultValue: "Bookmarks, settings, and extensions import are not available yet."
)
noteLabel.font = NSFont.systemFont(ofSize: 12)
noteLabel.textColor = .secondaryLabelColor
noteLabel.maximumNumberOfLines = 2
noteLabel.preferredMaxLayoutWidth = 540
additionalDataNoteLabel.font = NSFont.systemFont(ofSize: 11)
additionalDataNoteLabel.textColor = .secondaryLabelColor
additionalDataNoteLabel.maximumNumberOfLines = 2
additionalDataNoteLabel.preferredMaxLayoutWidth = 500
additionalDataNoteLabel.isHidden = true
dataTypesContainer.orientation = .vertical
dataTypesContainer.spacing = 8
dataTypesContainer.spacing = 6
dataTypesContainer.alignment = .leading
dataTypesContainer.addArrangedSubview(destinationTitleLabel)
dataTypesContainer.addArrangedSubview(destinationModeContainer)
@ -8843,13 +8892,14 @@ final class BrowserDataImportCoordinator {
dataTypesContainer.addArrangedSubview(cookiesCheckbox)
dataTypesContainer.addArrangedSubview(historyCheckbox)
dataTypesContainer.addArrangedSubview(additionalDataCheckbox)
dataTypesContainer.addArrangedSubview(additionalDataNoteLabel)
dataTypesContainer.addArrangedSubview(domainRow)
dataTypesContainer.addArrangedSubview(noteLabel)
}
private func configureInitialState() {
step = .source
refreshSourceProfilesList()
updateAdditionalDataNoteVisibility()
updateStepUI()
}
@ -8858,7 +8908,7 @@ final class BrowserDataImportCoordinator {
case .source:
stepLabel.stringValue = String(
localized: "browser.import.step.source",
defaultValue: "Step 1 of 3: Choose the browser to import from."
defaultValue: "Step 1 of 3"
)
sourceContainer.isHidden = false
sourceProfilesContainer.isHidden = true
@ -8868,11 +8918,8 @@ final class BrowserDataImportCoordinator {
primaryButton.title = String(localized: "browser.import.next", defaultValue: "Next")
case .sourceProfiles:
stepLabel.stringValue = String(
format: String(
localized: "browser.import.step.sourceProfiles",
defaultValue: "Step 2 of 3: Choose source profiles from %@."
),
selectedBrowser().displayName
localized: "browser.import.step.sourceProfiles",
defaultValue: "Step 2 of 3"
)
sourceContainer.isHidden = true
sourceProfilesContainer.isHidden = false
@ -8883,11 +8930,8 @@ final class BrowserDataImportCoordinator {
case .dataTypes:
rebuildStep3DestinationUI()
stepLabel.stringValue = String(
format: String(
localized: "browser.import.step.dataTypes",
defaultValue: "Step 3 of 3: Choose what to import from %@ and where to put it."
),
selectedBrowser().displayName
localized: "browser.import.step.dataTypes",
defaultValue: "Step 3 of 3"
)
sourceContainer.isHidden = true
sourceProfilesContainer.isHidden = true
@ -8899,6 +8943,7 @@ final class BrowserDataImportCoordinator {
defaultValue: "Start Import"
)
}
updatePanelSize()
}
private func selectedBrowser() -> InstalledBrowserCandidate {
@ -8925,6 +8970,7 @@ final class BrowserDataImportCoordinator {
browser.displayName
)
sourceProfilesList.addArrangedSubview(sourceProfilesEmptyLabel)
updateSourceProfilesPresentation(for: browser)
return
}
@ -8940,6 +8986,8 @@ final class BrowserDataImportCoordinator {
sourceProfilesList.addArrangedSubview(checkbox)
sourceProfileCheckboxes.append(checkbox)
}
updateSourceProfilesPresentation(for: browser)
}
private func storedSelectedSourceProfileIDs(for browser: InstalledBrowserCandidate) -> Set<String> {
@ -9055,16 +9103,16 @@ final class BrowserDataImportCoordinator {
localized: "browser.import.destinationProfile.separateHelp",
defaultValue: "Missing cmux profiles are created when import starts."
)
destinationHelpLabel.isHidden = false
} else if plan.entries.count > 1 {
destinationHelpLabel.stringValue = String(
localized: "browser.import.destinationProfile.mergeHelp",
defaultValue: "All selected source profiles will be merged into the chosen cmux browser profile."
)
destinationHelpLabel.isHidden = false
} else {
destinationHelpLabel.stringValue = String(
localized: "browser.import.destinationProfile.help",
defaultValue: "Imported cookies and history go into the selected cmux browser profile."
)
destinationHelpLabel.stringValue = ""
destinationHelpLabel.isHidden = true
}
}
@ -9081,7 +9129,7 @@ final class BrowserDataImportCoordinator {
guard let sourceProfile = entry.sourceProfiles.first else { continue }
let sourceLabel = NSTextField(labelWithString: sourceProfile.displayName)
sourceLabel.alignment = .right
sourceLabel.frame.size.width = 140
sourceLabel.frame.size.width = 110
let popup = NSPopUpButton(frame: .zero, pullsDown: false)
popup.target = self
@ -9101,11 +9149,14 @@ final class BrowserDataImportCoordinator {
} else {
popup.selectItem(at: 0)
}
popup.setContentHuggingPriority(.defaultLow, for: .horizontal)
popup.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
let row = NSStackView(views: [sourceLabel, popup])
row.orientation = .horizontal
row.spacing = 8
row.spacing = 6
row.alignment = .centerY
row.distribution = .fill
separateDestinationRows.addArrangedSubview(row)
}
}
@ -9137,7 +9188,7 @@ final class BrowserDataImportCoordinator {
)
)
destinationLabel.alignment = .right
destinationLabel.frame.size.width = 140
destinationLabel.frame.size.width = 110
mergeDestinationRow.addArrangedSubview(destinationLabel)
mergeDestinationRow.addArrangedSubview(mergeDestinationPopup)
@ -9211,6 +9262,51 @@ final class BrowserDataImportCoordinator {
return base.isEmpty ? "profile-\(index)" : base
}
private func updateSourceProfilesPresentation(for browser: InstalledBrowserCandidate) {
let presentation = BrowserImportSourceProfilesPresentation(profileCount: browser.profiles.count)
sourceProfilesScrollHeightConstraint?.constant = presentation.scrollHeight
sourceProfilesHelpLabel.isHidden = !presentation.showsHelpText
}
private func updateAdditionalDataNoteVisibility() {
additionalDataNoteLabel.isHidden = additionalDataCheckbox.state != .on
}
private func updatePanelSize() {
let contentSize = preferredContentSize()
let targetFrame = panel.frameRect(forContentRect: NSRect(origin: .zero, size: contentSize))
guard panel.frame.size != targetFrame.size else { return }
if !panel.isVisible {
panel.setContentSize(contentSize)
return
}
var frame = panel.frame
frame.origin.x -= (targetFrame.width - frame.width) / 2
frame.origin.y -= (targetFrame.height - frame.height) / 2
frame.size = targetFrame.size
panel.setFrame(frame, display: true)
}
private func preferredContentSize() -> NSSize {
switch step {
case .source:
return NSSize(width: 560, height: 292)
case .sourceProfiles:
let presentation = BrowserImportSourceProfilesPresentation(profileCount: selectedBrowser().profiles.count)
let helpHeight: CGFloat = presentation.showsHelpText ? 24 : 0
let height = 214 + presentation.scrollHeight + helpHeight
return NSSize(width: 560, height: min(max(height, 292), 360))
case .dataTypes:
var height: CGFloat = currentExecutionPlan().mode == .separateProfiles ? 412 : 374
if additionalDataCheckbox.state == .on {
height += 24
}
return NSSize(width: 560, height: height)
}
}
private func finishModal(with response: NSApplication.ModalResponse) {
guard !didFinishModal else { return }
didFinishModal = true

View file

@ -127,6 +127,23 @@ final class BrowserImportMappingTests: XCTestCase {
XCTAssertTrue(presentation.showsSingleDestinationPicker)
}
func testSourceProfilesPresentationShrinksListForSmallProfileCounts() {
let presentation = BrowserImportSourceProfilesPresentation(profileCount: 2)
XCTAssertEqual(presentation.scrollHeight, 76)
XCTAssertTrue(presentation.showsHelpText)
}
func testSourceProfilesPresentationCapsListHeightAndHidesHelpForSingleProfile() {
let singleProfilePresentation = BrowserImportSourceProfilesPresentation(profileCount: 1)
let manyProfilesPresentation = BrowserImportSourceProfilesPresentation(profileCount: 9)
XCTAssertEqual(singleProfilePresentation.scrollHeight, 76)
XCTAssertFalse(singleProfilePresentation.showsHelpText)
XCTAssertEqual(manyProfilesPresentation.scrollHeight, 144)
XCTAssertTrue(manyProfilesPresentation.showsHelpText)
}
@MainActor
func testRealizePlanCreatesMissingDestinationProfilesOnlyWhenRequested() throws {
let suiteName = "BrowserImportMappingTests-\(UUID().uuidString)"

View file

@ -19,10 +19,10 @@ final class BrowserImportProfilesUITests: XCTestCase {
app.buttons["Next"].click()
XCTAssertTrue(
app.radioButtons["Keep profiles separate"].waitForExistence(timeout: 5.0),
app.radioButtons["Separate profiles"].waitForExistence(timeout: 5.0),
"Expected Step 3 to show the separate-profiles default"
)
XCTAssertTrue(app.radioButtons["Merge all into one cmux profile"].exists)
XCTAssertTrue(app.radioButtons["Merge into one"].exists)
XCTAssertTrue(app.popUpButtons["BrowserImportDestinationPopup-you"].exists)
XCTAssertTrue(app.popUpButtons["BrowserImportDestinationPopup-austin"].exists)
@ -49,7 +49,7 @@ final class BrowserImportProfilesUITests: XCTestCase {
app.buttons["Next"].click()
app.buttons["Next"].click()
let mergeRadio = app.radioButtons["Merge all into one cmux profile"]
let mergeRadio = app.radioButtons["Merge into one"]
XCTAssertTrue(mergeRadio.waitForExistence(timeout: 5.0))
mergeRadio.click()