| @@ -11,6 +11,8 @@ struct AccountView: View { |
| 11 | @State private var verifyCode = "" |
11 | @State private var verifyCode = "" |
| 12 | @State private var removingEmail: AccountViewModel.EmailAddress? |
12 | @State private var removingEmail: AccountViewModel.EmailAddress? |
| 13 | @State private var removingSSH: AccountViewModel.SSHKey? |
13 | @State private var removingSSH: AccountViewModel.SSHKey? |
| |
14 | @State private var labellingSSH: AccountViewModel.SSHKey? |
| |
15 | @State private var sshLabelText = "" |
| 14 | @State private var removingPGP: AccountViewModel.PGPKey? |
16 | @State private var removingPGP: AccountViewModel.PGPKey? |
| 15 | |
17 | |
| 16 | init(client: GitbayClient) { |
18 | init(client: GitbayClient) { |
| @@ -87,9 +89,9 @@ struct AccountView: View { |
| 87 | scopes: ["full", "git"], |
89 | scopes: ["full", "git"], |
| 88 | working: model.working, |
90 | working: model.working, |
| 89 | errorMessage: model.actionError |
91 | errorMessage: model.actionError |
| 90 | ) { text, scope in |
92 | ) { text, scope, label in |
| 91 | Task { |
93 | Task { |
| 92 | await model.addSSHKey(text, scope: scope ?? "full") |
94 | await model.addSSHKey(text, scope: scope ?? "full", label: label) |
| 93 | if model.actionError == nil { addingSSH = false } |
95 | if model.actionError == nil { addingSSH = false } |
| 94 | } |
96 | } |
| 95 | } |
97 | } |
| @@ -101,13 +103,32 @@ struct AccountView: View { |
| 101 | scopes: nil, |
103 | scopes: nil, |
| 102 | working: model.working, |
104 | working: model.working, |
| 103 | errorMessage: model.actionError |
105 | errorMessage: model.actionError |
| 104 | ) { text, _ in |
106 | ) { text, _, _ in |
| 105 | Task { |
107 | Task { |
| 106 | await model.addPGPKey(text) |
108 | await model.addPGPKey(text) |
| 107 | if model.actionError == nil { addingPGP = false } |
109 | if model.actionError == nil { addingPGP = false } |
| 108 | } |
110 | } |
| 109 | } |
111 | } |
| 110 | } |
112 | } |
| |
113 | .alert( |
| |
114 | "Label this key", |
| |
115 | isPresented: Binding( |
| |
116 | get: { labellingSSH != nil }, |
| |
117 | set: { if !$0 { labellingSSH = nil } } |
| |
118 | ), |
| |
119 | presenting: labellingSSH |
| |
120 | ) { key in |
| |
121 | TextField("Label", text: $sshLabelText) |
| |
122 | .autocorrectionDisabled() |
| |
123 | .textInputAutocapitalization(.never) |
| |
124 | Button("Save") { |
| |
125 | Task { await model.labelSSHKey(key, sshLabelText) } |
| |
126 | labellingSSH = nil |
| |
127 | } |
| |
128 | Button("Cancel", role: .cancel) { labellingSSH = nil } |
| |
129 | } message: { _ in |
| |
130 | Text("An empty label clears it.") |
| |
131 | } |
| 111 | .confirmationDialog( |
132 | .confirmationDialog( |
| 112 | "Remove this SSH key? Anything authenticating with it loses access.", |
133 | "Remove this SSH key? Anything authenticating with it loses access.", |
| 113 | isPresented: Binding( |
134 | isPresented: Binding( |
| @@ -171,6 +192,10 @@ struct AccountView: View { |
| 171 | Section { |
192 | Section { |
| 172 | ForEach(keys) { key in |
193 | ForEach(keys) { key in |
| 173 | VStack(alignment: .leading, spacing: 2) { |
194 | VStack(alignment: .leading, spacing: 2) { |
| |
195 | if let label = key.label, !label.isEmpty { |
| |
196 | Text(label) |
| |
197 | .font(.gbSans(.subheadline).weight(.medium)) |
| |
198 | } |
| 174 | Text(key.fingerprint) |
199 | Text(key.fingerprint) |
| 175 | .font(.gbMono(.caption)) |
200 | .font(.gbMono(.caption)) |
| 176 | .lineLimit(1) |
201 | .lineLimit(1) |
| @@ -186,6 +211,11 @@ struct AccountView: View { |
| 186 | Button("Remove", role: .destructive) { |
211 | Button("Remove", role: .destructive) { |
| 187 | removingSSH = key |
212 | removingSSH = key |
| 188 | } |
213 | } |
| |
214 | Button("Label") { |
| |
215 | sshLabelText = key.label ?? "" |
| |
216 | labellingSSH = key |
| |
217 | } |
| |
218 | .tint(.gbAccent) |
| 189 | } |
219 | } |
| 190 | } |
220 | } |
| 191 | Button { |
221 | Button { |
| @@ -308,11 +338,13 @@ private struct KeyPasteSheet: View { |
| 308 | let scopes: [String]? |
338 | let scopes: [String]? |
| 309 | let working: Bool |
339 | let working: Bool |
| 310 | let errorMessage: String? |
340 | let errorMessage: String? |
| 311 | let onSubmit: (String, String?) -> Void |
341 | /// Text, scope (nil without scopes), label (empty without scopes). |
| |
342 | let onSubmit: (String, String?, String) -> Void |
| 312 | |
343 | |
| 313 | @Environment(\.dismiss) private var dismiss |
344 | @Environment(\.dismiss) private var dismiss |
| 314 | @State private var text = "" |
345 | @State private var text = "" |
| 315 | @State private var scope = "full" |
346 | @State private var scope = "full" |
| |
347 | @State private var label = "" |
| 316 | |
348 | |
| 317 | var body: some View { |
349 | var body: some View { |
| 318 | NavigationStack { |
350 | NavigationStack { |
| @@ -333,6 +365,10 @@ private struct KeyPasteSheet: View { |
| 333 | ForEach(scopes, id: \.self) { Text($0).tag($0) } |
365 | ForEach(scopes, id: \.self) { Text($0).tag($0) } |
| 334 | } |
366 | } |
| 335 | .pickerStyle(.segmented) |
367 | .pickerStyle(.segmented) |
| |
368 | TextField("Label (optional)", text: $label) |
| |
369 | .autocorrectionDisabled() |
| |
370 | .textInputAutocapitalization(.never) |
| |
371 | .accessibilityIdentifier("key-paste-label") |
| 336 | } |
372 | } |
| 337 | } |
373 | } |
| 338 | if let errorMessage { |
374 | if let errorMessage { |
| @@ -352,7 +388,7 @@ private struct KeyPasteSheet: View { |
| 352 | ProgressView() |
388 | ProgressView() |
| 353 | } else { |
389 | } else { |
| 354 | Button("Add") { |
390 | Button("Add") { |
| 355 | onSubmit(text, scopes != nil ? scope : nil) |
391 | onSubmit(text, scopes != nil ? scope : nil, scopes != nil ? label : "") |
| 356 | } |
392 | } |
| 357 | .disabled(text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) |
393 | .disabled(text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) |
| 358 | .accessibilityIdentifier("key-paste-submit") |
394 | .accessibilityIdentifier("key-paste-submit") |