@@ -10,6 +10,7 @@ import UniformTypeIdentifiers
1010
1111struct ConnectionImportSheet : View {
1212 let fileURL : URL
13+ var onImported : ( ( Int ) -> Void ) ?
1314 @Environment ( \. dismiss) private var dismiss
1415 @State private var preview : ConnectionImportPreview ?
1516 @State private var error : String ?
@@ -19,53 +20,82 @@ struct ConnectionImportSheet: View {
1920
2021 var body : some View {
2122 VStack ( spacing: 0 ) {
22- header
23- Divider ( )
24-
2523 if isLoading {
26- Spacer ( )
27- ProgressView ( )
28- . controlSize ( . large)
29- Spacer ( )
24+ loadingView
3025 } else if let error {
31- Spacer ( )
32- VStack ( spacing: 8 ) {
33- Image ( systemName: " exclamationmark.triangle " )
34- . font ( . largeTitle)
35- . foregroundStyle ( . secondary)
36- Text ( error)
37- . foregroundStyle ( . secondary)
38- . multilineTextAlignment ( . center)
39- }
40- . padding ( )
41- Spacer ( )
26+ errorView ( error)
4227 } else if let preview {
28+ header ( preview)
29+ Divider ( )
4330 previewList ( preview)
4431 Divider ( )
4532 footer ( preview)
4633 }
4734 }
48- . frame ( width: 480 , height: 420 )
35+ . frame ( width: 500 , height: 400 )
4936 . onAppear { loadFile ( ) }
5037 }
5138
39+ // MARK: - Loading
40+
41+ private var loadingView : some View {
42+ VStack {
43+ Spacer ( )
44+ ProgressView ( )
45+ . controlSize ( . large)
46+ Spacer ( )
47+ }
48+ . frame ( height: 200 )
49+ }
50+
51+ // MARK: - Error
52+
53+ private func errorView( _ message: String ) -> some View {
54+ VStack ( spacing: 12 ) {
55+ Spacer ( )
56+ Image ( systemName: " exclamationmark.triangle " )
57+ . font ( . system( size: 32 ) )
58+ . foregroundStyle ( . secondary)
59+ Text ( message)
60+ . foregroundStyle ( . secondary)
61+ . multilineTextAlignment ( . center)
62+ Spacer ( )
63+ HStack {
64+ Spacer ( )
65+ Button ( String ( localized: " OK " ) ) { dismiss ( ) }
66+ . buttonStyle ( . borderedProminent)
67+ . keyboardShortcut ( . defaultAction)
68+ }
69+ . padding ( 12 )
70+ }
71+ . padding ( . horizontal)
72+ }
73+
5274 // MARK: - Header
5375
54- private var header : some View {
76+ private func header( _ preview : ConnectionImportPreview ) -> some View {
5577 HStack {
56- Image ( systemName: " square.and.arrow.down " )
57- . font ( . title2)
58- . foregroundStyle ( Color . accentColor)
59- VStack ( alignment: . leading, spacing: 2 ) {
60- Text ( " Import Connections " )
61- . font ( . headline)
62- Text ( fileURL. lastPathComponent)
63- . font ( . caption)
64- . foregroundStyle ( . secondary)
65- }
78+ Text ( " Import Connections " )
79+ . font ( . system( size: 13 , weight: . semibold) )
80+ Text ( " ( \( fileURL. lastPathComponent) ) " )
81+ . font ( . system( size: 13 ) )
82+ . foregroundStyle ( . secondary)
6683 Spacer ( )
84+ Toggle ( String ( localized: " Select All " ) , isOn: Binding (
85+ get: { selectedIds. count == preview. items. count && !preview. items. isEmpty } ,
86+ set: { newValue in
87+ if newValue {
88+ selectedIds = Set ( preview. items. map ( \. id) )
89+ } else {
90+ selectedIds. removeAll ( )
91+ }
92+ }
93+ ) )
94+ . toggleStyle ( . checkbox)
95+ . controlSize ( . small)
6796 }
68- . padding ( )
97+ . padding ( . vertical, 10 )
98+ . padding ( . horizontal, 16 )
6999 }
70100
71101 // MARK: - Preview List
@@ -82,7 +112,7 @@ struct ConnectionImportSheet: View {
82112 @ViewBuilder
83113 private func importItemRow( _ item: ImportItem ) -> some View {
84114 let isSelected = selectedIds. contains ( item. id)
85- HStack ( spacing: 10 ) {
115+ HStack ( spacing: 8 ) {
86116 Toggle ( " " , isOn: Binding (
87117 get: { isSelected } ,
88118 set: { newValue in
@@ -97,67 +127,88 @@ struct ConnectionImportSheet: View {
97127 . labelsHidden ( )
98128
99129 DatabaseType ( rawValue: item. connection. type) . iconImage
100- . frame ( width: 20 , height: 20 )
101-
102- VStack ( alignment: . leading, spacing: 2 ) {
103- Text ( item. connection. name)
104- . fontWeight ( . semibold)
105- Text ( " \( item. connection. host) : \( String ( item. connection. port) ) " )
106- . font ( . caption)
107- . foregroundStyle ( . secondary)
130+ . frame ( width: 18 , height: 18 )
131+
132+ VStack ( alignment: . leading, spacing: 1 ) {
133+ HStack ( spacing: 4 ) {
134+ Text ( item. connection. name)
135+ . font ( . system( size: 13 ) )
136+ . lineLimit ( 1 )
137+ if case . duplicate = item. status {
138+ Text ( String ( localized: " duplicate " ) )
139+ . font ( . system( size: 10 ) )
140+ . foregroundStyle ( . secondary)
141+ . padding ( . horizontal, 4 )
142+ . padding ( . vertical, 1 )
143+ . background (
144+ RoundedRectangle ( cornerRadius: 3 )
145+ . fill ( Color ( nsColor: . quaternaryLabelColor) )
146+ )
147+ }
148+ }
149+ HStack ( spacing: 0 ) {
150+ Text ( " \( item. connection. host) : \( String ( item. connection. port) ) " )
151+ warningText ( for: item. status)
152+ }
153+ . font ( . system( size: 11 ) )
154+ . foregroundStyle ( . secondary)
155+ . lineLimit ( 1 )
108156 }
109157
110158 Spacer ( )
111159
112- statusBadge ( for: item. status)
160+ if case . duplicate = item. status, isSelected {
161+ Picker ( " " , selection: Binding (
162+ get: { duplicateResolutions [ item. id] ?? . importAsCopy } ,
163+ set: { duplicateResolutions [ item. id] = $0 }
164+ ) ) {
165+ Text ( " As Copy " ) . tag ( ImportResolution . importAsCopy)
166+ if case . duplicate( let existing) = item. status {
167+ Text ( " Replace " ) . tag ( ImportResolution . replace ( existingId: existing. id) )
168+ }
169+ Text ( " Skip " ) . tag ( ImportResolution . skip)
170+ }
171+ . pickerStyle ( . menu)
172+ . controlSize ( . small)
173+ . frame ( width: 110 )
174+ . labelsHidden ( )
175+ } else {
176+ statusIcon ( for: item. status)
177+ }
113178 }
114179 . padding ( . vertical, 2 )
115-
116- if case . duplicate = item. status, isSelected {
117- duplicateResolutionPicker ( for: item)
118- . padding ( . leading, 36 )
119- }
120180 }
121181
122182 @ViewBuilder
123- private func statusBadge ( for status: ImportItemStatus ) -> some View {
183+ private func statusIcon ( for status: ImportItemStatus ) -> some View {
124184 switch status {
125185 case . ready:
126- Image ( systemName: " circle.fill " )
127- . font ( . caption2 )
186+ Image ( systemName: " checkmark. circle.fill" )
187+ . font ( . system ( size : 12 ) )
128188 . foregroundStyle ( . green)
129189 case . warnings:
130190 Image ( systemName: " exclamationmark.triangle.fill " )
131- . font ( . caption2 )
191+ . font ( . system ( size : 12 ) )
132192 . foregroundStyle ( . yellow)
133193 case . duplicate:
134- Image ( systemName: " circle.fill " )
135- . font ( . caption2)
136- . foregroundStyle ( . orange)
194+ EmptyView ( )
137195 }
138196 }
139197
140- private func duplicateResolutionPicker( for item: ImportItem ) -> some View {
141- Picker ( String ( localized: " Action " ) , selection: Binding (
142- get: { duplicateResolutions [ item. id] ?? . skip } ,
143- set: { duplicateResolutions [ item. id] = $0 }
144- ) ) {
145- Text ( " Skip " ) . tag ( ImportResolution . skip)
146- if case . duplicate( let existing) = item. status {
147- Text ( " Replace Existing " ) . tag ( ImportResolution . replace ( existingId: existing. id) )
148- }
149- Text ( " Import as Copy " ) . tag ( ImportResolution . importAsCopy)
198+ @ViewBuilder
199+ private func warningText( for status: ImportItemStatus ) -> some View {
200+ if case . warnings( let messages) = status, let first = messages. first {
201+ Text ( " — \( first) " )
202+ . foregroundStyle ( . orange)
150203 }
151- . pickerStyle ( . segmented)
152- . controlSize ( . small)
153204 }
154205
155206 // MARK: - Footer
156207
157208 private func footer( _ preview: ConnectionImportPreview ) -> some View {
158209 HStack {
159- Text ( " \( selectedIds. count) of \( preview. items. count) connections selected " )
160- . font ( . caption )
210+ Text ( " \( selectedIds. count) of \( preview. items. count) selected " )
211+ . font ( . system ( size : 11 ) )
161212 . foregroundStyle ( . secondary)
162213
163214 Spacer ( )
@@ -170,10 +221,11 @@ struct ConnectionImportSheet: View {
170221 Button ( String ( localized: " Import " ) ) {
171222 performImport ( preview)
172223 }
224+ . buttonStyle ( . borderedProminent)
173225 . keyboardShortcut ( . defaultAction)
174226 . disabled ( selectedIds. isEmpty)
175227 }
176- . padding ( )
228+ . padding ( 12 )
177229 }
178230
179231 // MARK: - Actions
@@ -184,12 +236,13 @@ struct ConnectionImportSheet: View {
184236 let result = ConnectionExportService . analyzeImport ( envelope)
185237 preview = result
186238
187- // Pre-select non-duplicate items
239+ // Pre-select non-duplicate items only
188240 for item in result. items {
189241 switch item. status {
190242 case . ready, . warnings:
191243 selectedIds. insert ( item. id)
192244 case . duplicate:
245+ // Duplicates unchecked by default — user opts in
193246 break
194247 }
195248 }
@@ -207,16 +260,15 @@ struct ConnectionImportSheet: View {
207260 case . ready, . warnings:
208261 resolutions [ item. id] = . importNew
209262 case . duplicate:
210- resolutions [ item. id] = duplicateResolutions [ item. id] ?? . skip
263+ resolutions [ item. id] = duplicateResolutions [ item. id] ?? . importAsCopy
211264 }
212265 } else {
213266 resolutions [ item. id] = . skip
214267 }
215268 }
216269
217- ConnectionExportService . performImport ( preview, resolutions: resolutions)
270+ let count = ConnectionExportService . performImport ( preview, resolutions: resolutions)
218271 dismiss ( )
272+ onImported ? ( count)
219273 }
220274}
221-
222-
0 commit comments