Commit 3ee1c43ddb
Verified · cmc
Layout: unified · split
gitbay/Views/Builds/BuildDetailView.swift +36
| @@ -4,6 +4,7 @@ import SwiftUI | |||
| 4 | struct BuildDetailView: View { | 4 | struct BuildDetailView: View { |
| 5 | 5 | ||
| 6 | @State private var model: BuildDetailViewModel | 6 | @State private var model: BuildDetailViewModel |
| 7 | @State private var confirmingCancel = false | ||
| 7 | 8 | ||
| 8 | init(client: GitbayClient, repo: String, number: Int64) { | 9 | init(client: GitbayClient, repo: String, number: Int64) { |
| 9 | _model = State(initialValue: BuildDetailViewModel( | 10 | _model = State(initialValue: BuildDetailViewModel( |
| @@ -16,6 +17,11 @@ struct BuildDetailView: View { | |||
| 16 | Color.clear | 17 | Color.clear |
| 17 | if let detail = model.state.value { | 18 | if let detail = model.state.value { |
| 18 | VStack(spacing: 0) { | 19 | VStack(spacing: 0) { |
| 20 | if let error = model.actionError { | ||
| 21 | GBNotice(error, .gbWarn) | ||
| 22 | .padding(.horizontal, 16) | ||
| 23 | .padding(.vertical, 8) | ||
| 24 | } | ||
| 19 | header(detail.build) | 25 | header(detail.build) |
| 20 | Divider() | 26 | Divider() |
| 21 | log(detail.log) | 27 | log(detail.log) |
| @@ -25,8 +31,38 @@ struct BuildDetailView: View { | |||
| 25 | .overlay { LoadStateOverlay(state: model.state) } | 31 | .overlay { LoadStateOverlay(state: model.state) } |
| 26 | .navigationTitle("Build #\(model.number)") | 32 | .navigationTitle("Build #\(model.number)") |
| 27 | .navigationBarTitleDisplayMode(.inline) | 33 | .navigationBarTitleDisplayMode(.inline) |
| 34 | .toolbar { | ||
| 35 | ToolbarItem(placement: .topBarTrailing) { | ||
| 36 | if model.isCancellable { | ||
| 37 | if model.working { | ||
| 38 | ProgressView() | ||
| 39 | } else { | ||
| 40 | Button("Cancel", role: .destructive) { | ||
| 41 | confirmingCancel = true | ||
| 42 | } | ||
| 43 | .accessibilityIdentifier("build-cancel-button") | ||
| 44 | } | ||
| 45 | } | ||
| 46 | } | ||
| 47 | } | ||
| 28 | .task { await model.load() } | 48 | .task { await model.load() } |
| 29 | .refreshable { await model.load() } | 49 | .refreshable { await model.load() } |
| 50 | .confirmationDialog( | ||
| 51 | "Cancel this build?", | ||
| 52 | isPresented: $confirmingCancel, | ||
| 53 | titleVisibility: .visible | ||
| 54 | ) { | ||
| 55 | Button("Cancel Build", role: .destructive) { | ||
| 56 | Task { await model.cancel() } | ||
| 57 | } | ||
| 58 | Button("Keep Running", role: .cancel) {} | ||
| 59 | } message: { | ||
| 60 | Text( | ||
| 61 | "A running step ends within a couple of seconds. If this is a " + | ||
| 62 | "duplicate of a commit that already passed, that earlier result " + | ||
| 63 | "is restored on the commit." | ||
| 64 | ) | ||
| 65 | } | ||
| 30 | } | 66 | } |
| 31 | 67 | ||
| 32 | private func header(_ build: Build) -> some View { | 68 | private func header(_ build: Build) -> some View { |
gitbay/Views/Discovery/ProfileView.swift +33
| @@ -4,7 +4,10 @@ import SwiftUI | |||
| 4 | /// have been doing, and what they own that you can see. | 4 | /// have been doing, and what they own that you can see. |
| 5 | struct ProfileView: View { | 5 | struct ProfileView: View { |
| 6 | 6 | ||
| 7 | @Environment(SessionStore.self) private var session | ||
| 7 | @State private var model: ProfileViewModel | 8 | @State private var model: ProfileViewModel |
| 9 | @State private var creatingOrg = false | ||
| 10 | @State private var newOrgName = "" | ||
| 8 | 11 | ||
| 9 | /// Someone else's profile is titled with their name. Your own is | 12 | /// Someone else's profile is titled with their name. Your own is |
| 10 | /// reached from a tab and titled to match it. | 13 | /// reached from a tab and titled to match it. |
| @@ -18,6 +21,11 @@ struct ProfileView: View { | |||
| 18 | var body: some View { | 21 | var body: some View { |
| 19 | List { | 22 | List { |
| 20 | if let profile = model.state.value { | 23 | if let profile = model.state.value { |
| 24 | if let error = model.actionError { | ||
| 25 | Section { | ||
| 26 | GBNotice(error, .gbWarn) | ||
| 27 | } | ||
| 28 | } | ||
| 21 | header(profile) | 29 | header(profile) |
| 22 | peopleSection(profile) | 30 | peopleSection(profile) |
| 23 | aboutSection(profile) | 31 | aboutSection(profile) |
| @@ -35,8 +43,33 @@ struct ProfileView: View { | |||
| 35 | .overlay { LoadStateOverlay(state: model.state) } | 43 | .overlay { LoadStateOverlay(state: model.state) } |
| 36 | .navigationTitle(title ?? model.name) | 44 | .navigationTitle(title ?? model.name) |
| 37 | .navigationBarTitleDisplayMode(.inline) | 45 | .navigationBarTitleDisplayMode(.inline) |
| 46 | .toolbar { | ||
| 47 | if let profile = model.state.value, | ||
| 48 | !profile.isOrg, session.current?.username == profile.name { | ||
| 49 | ToolbarItem(placement: .topBarTrailing) { | ||
| 50 | Button { | ||
| 51 | newOrgName = "" | ||
| 52 | creatingOrg = true | ||
| 53 | } label: { | ||
| 54 | Image(systemName: "plus.circle") | ||
| 55 | } | ||
| 56 | .disabled(model.working) | ||
| 57 | .accessibilityIdentifier("org-create-button") | ||
| 58 | } | ||
| 59 | } | ||
| 60 | } | ||
| 38 | .task { await model.load() } | 61 | .task { await model.load() } |
| 39 | .refreshable { await model.load() } | 62 | .refreshable { await model.load() } |
| 63 | .alert("New Organization", isPresented: $creatingOrg) { | ||
| 64 | TextField("Name", text: $newOrgName) | ||
| 65 | .autocorrectionDisabled() | ||
| 66 | .textInputAutocapitalization(.never) | ||
| 67 | Button("Create") { | ||
| 68 | Task { await model.createOrg(newOrgName) } | ||
| 69 | } | ||
| 70 | .disabled(newOrgName.trimmingCharacters(in: .whitespaces).isEmpty) | ||
| 71 | Button("Cancel", role: .cancel) {} | ||
| 72 | } | ||
| 40 | } | 73 | } |
| 41 | 74 | ||
| 42 | // MARK: - Sections | 75 | // MARK: - Sections |
gitbay/Views/Orgs/OrgView.swift +26
| @@ -10,6 +10,8 @@ struct OrgView: View { | |||
| 10 | @State private var newTeam = "" | 10 | @State private var newTeam = "" |
| 11 | @State private var removingMember: OrgMember? | 11 | @State private var removingMember: OrgMember? |
| 12 | @State private var deletingTeam: String? | 12 | @State private var deletingTeam: String? |
| 13 | @State private var renamingOrg = false | ||
| 14 | @State private var newOrgName = "" | ||
| 13 | 15 | ||
| 14 | init(client: GitbayClient, org: String) { | 16 | init(client: GitbayClient, org: String) { |
| 15 | _model = State(initialValue: OrgViewModel(client: client, orgName: org)) | 17 | _model = State(initialValue: OrgViewModel(client: client, orgName: org)) |
| @@ -30,8 +32,32 @@ struct OrgView: View { | |||
| 30 | .overlay { LoadStateOverlay(state: model.state) } | 32 | .overlay { LoadStateOverlay(state: model.state) } |
| 31 | .navigationTitle(model.orgName) | 33 | .navigationTitle(model.orgName) |
| 32 | .navigationBarTitleDisplayMode(.inline) | 34 | .navigationBarTitleDisplayMode(.inline) |
| 35 | .toolbar { | ||
| 36 | ToolbarItem(placement: .topBarTrailing) { | ||
| 37 | Button { | ||
| 38 | newOrgName = model.orgName | ||
| 39 | renamingOrg = true | ||
| 40 | } label: { | ||
| 41 | Image(systemName: "pencil") | ||
| 42 | } | ||
| 43 | .disabled(model.working) | ||
| 44 | .accessibilityIdentifier("org-rename-button") | ||
| 45 | } | ||
| 46 | } | ||
| 33 | .task { await model.load() } | 47 | .task { await model.load() } |
| 34 | .refreshable { await model.load() } | 48 | .refreshable { await model.load() } |
| 49 | .alert("Rename \(model.orgName)", isPresented: $renamingOrg) { | ||
| 50 | TextField("New name", text: $newOrgName) | ||
| 51 | .autocorrectionDisabled() | ||
| 52 | .textInputAutocapitalization(.never) | ||
| 53 | Button("Rename") { | ||
| 54 | Task { await model.rename(to: newOrgName) } | ||
| 55 | } | ||
| 56 | .disabled(newOrgName.trimmingCharacters(in: .whitespaces).isEmpty) | ||
| 57 | Button("Cancel", role: .cancel) {} | ||
| 58 | } message: { | ||
| 59 | Text("Clone URLs for every repository in this organization change to the new name.") | ||
| 60 | } | ||
| 35 | .confirmationDialog( | 61 | .confirmationDialog( |
| 36 | "Remove \(removingMember?.user ?? "") from \(model.orgName)?", | 62 | "Remove \(removingMember?.user ?? "") from \(model.orgName)?", |
| 37 | isPresented: Binding( | 63 | isPresented: Binding( |
gitbay/Views/Releases/ReleaseView.swift +27
| @@ -6,6 +6,8 @@ struct ReleaseView: View { | |||
| 6 | @State private var editing = false | 6 | @State private var editing = false |
| 7 | @State private var draftTitle = "" | 7 | @State private var draftTitle = "" |
| 8 | @State private var draftNotes = "" | 8 | @State private var draftNotes = "" |
| 9 | @State private var confirmingDelete = false | ||
| 10 | @Environment(\.dismiss) private var dismiss | ||
| 9 | 11 | ||
| 10 | init(client: GitbayClient, repo: String, tag: String) { | 12 | init(client: GitbayClient, repo: String, tag: String) { |
| 11 | _model = State(initialValue: ReleaseDetailViewModel( | 13 | _model = State(initialValue: ReleaseDetailViewModel( |
| @@ -89,6 +91,31 @@ struct ReleaseView: View { | |||
| 89 | .accessibilityIdentifier("release-edit-button") | 91 | .accessibilityIdentifier("release-edit-button") |
| 90 | } | 92 | } |
| 91 | } | 93 | } |
| 94 | ToolbarItem(placement: .topBarTrailing) { | ||
| 95 | if model.state.value != nil { | ||
| 96 | Button("Delete", role: .destructive) { | ||
| 97 | confirmingDelete = true | ||
| 98 | } | ||
| 99 | .disabled(model.working) | ||
| 100 | .accessibilityIdentifier("release-delete-button") | ||
| 101 | } | ||
| 102 | } | ||
| 103 | } | ||
| 104 | .confirmationDialog( | ||
| 105 | "Delete \(model.tag)?", | ||
| 106 | isPresented: $confirmingDelete, | ||
| 107 | titleVisibility: .visible | ||
| 108 | ) { | ||
| 109 | Button("Delete", role: .destructive) { | ||
| 110 | Task { | ||
| 111 | if await model.delete() { | ||
| 112 | dismiss() | ||
| 113 | } | ||
| 114 | } | ||
| 115 | } | ||
| 116 | Button("Cancel", role: .cancel) {} | ||
| 117 | } message: { | ||
| 118 | Text("This deletes the release's assets along with it.") | ||
| 92 | } | 119 | } |
| 93 | .sheet(isPresented: $editing) { | 120 | .sheet(isPresented: $editing) { |
| 94 | ComposeSheet( | 121 | ComposeSheet( |