Commit 0e79b13ed6

0e79b13ed6b1865df38f3752eece7d424eec9768

parent: 7584e76f6d

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-06 23:37 UTC

Repository actions in the repo menu

Layout: unified · split

gitbay/Views/Repos/RepoView.swift +70 −3
@@ -3,25 +3,37 @@ import SwiftUI
33struct RepoView: View {
44
55 @State private var model: RepoDetailViewModel
6 @State private var actionsModel: RepoActionsViewModel
7 private let client: GitbayClient
68 private let path: String
79 private let host: String
810
911 init(client: GitbayClient, path: String) {
12 self.client = client
1013 self.path = path
1114 self.host = client.instance.baseURL.host() ?? ""
1215 _model = State(initialValue: RepoDetailViewModel(client: client, path: path))
16 _actionsModel = State(initialValue: RepoActionsViewModel(client: client, repoPath: path))
1317 }
1418
1519 @State private var confirmingArchive = false
20 @State private var confirmingFork = false
21 @State private var forkName = ""
22 @State private var forkDestination: RepoRoute?
1623
1724 var body: some View {
1825 List {
1926 if let detail = model.state.value {
20 if let error = model.actionError {
27 if let error = model.actionError ?? actionsModel.actionError {
2128 Section {
2229 GBNotice(error, .gbWarn)
2330 }
2431 }
32 if let notice = actionsModel.notice {
33 Section {
34 GBNotice(notice, .gbOK)
35 }
36 }
2537 header(detail)
2638
2739 Section {
@@ -84,7 +96,26 @@ struct RepoView: View {
8496 .navigationBarTitleDisplayMode(.inline)
8597 .toolbar { toolbar }
8698 .task { await model.load() }
99 .task { await actionsModel.loadBookmarkState() }
87100 .refreshable { await model.load() }
101 .navigationDestination(item: $forkDestination) { route in
102 if case .repo(let forkedPath) = route {
103 RepoView(client: client, path: forkedPath)
104 }
105 }
106 .alert("Fork \(path)", isPresented: $confirmingFork) {
107 TextField("New name (optional)", text: $forkName)
108 .autocorrectionDisabled()
109 .textInputAutocapitalization(.never)
110 Button("Fork") {
111 Task {
112 if let result = await actionsModel.fork(named: forkName) {
113 forkDestination = .repo(result.path)
114 }
115 }
116 }
117 Button("Cancel", role: .cancel) {}
118 }
88119 .confirmationDialog(
89120 model.state.value?.isArchived == true
90121 ? "Unarchive \(path)?"
@@ -114,6 +145,42 @@ struct RepoView: View {
114145 systemImage: pinned ? "pin.slash" : "pin")
115146 }
116147 }
148 if let bookmarked = actionsModel.isBookmarked {
149 Button {
150 Task { await actionsModel.setBookmarked(!bookmarked) }
151 } label: {
152 Label(bookmarked ? "Unbookmark" : "Bookmark",
153 systemImage: bookmarked ? "bookmark.slash" : "bookmark")
154 }
155 } else {
156 Label {
157 Text("Checking Bookmark…")
158 } icon: {
159 ProgressView()
160 }
161 }
162 Divider()
163 Section {
164 Button {
165 Task { await actionsModel.watch() }
166 } label: {
167 Label("Watch this repository", systemImage: "eye")
168 }
169 Button {
170 Task { await actionsModel.unwatch() }
171 } label: {
172 Label("Mute this repository", systemImage: "bell.slash")
173 }
174 // Nothing reports which of these is current (krz/gitbay#178).
175 Text("The current setting isn't shown.")
176 }
177 Divider()
178 Button {
179 forkName = ""
180 confirmingFork = true
181 } label: {
182 Label("Fork", systemImage: "tuningfork")
183 }
117184 Divider()
118185 Button(role: detail.isArchived ? nil : .destructive) {
119186 confirmingArchive = true
@@ -122,13 +189,13 @@ struct RepoView: View {
122189 systemImage: "archivebox")
123190 }
124191 } label: {
125 if model.working {
192 if model.working || actionsModel.working {
126193 ProgressView()
127194 } else {
128195 Image(systemName: "ellipsis.circle")
129196 }
130197 }
131 .disabled(model.working)
198 .disabled(model.working || actionsModel.working)
132199 .accessibilityIdentifier("repo-actions-menu")
133200 }
134201 }
gitbayTests/RepoActionTests.swift +9 −4
@@ -53,15 +53,20 @@ struct RepoActionsTests {
5353 #expect(model.isBookmarked == false)
5454 }
5555
56 /// "We don't know" is not "not bookmarked". A failed load must leave
57 /// the state unknown rather than claiming the repository is not
58 /// bookmarked, which would draw the wrong control.
56 /// "We don't know" is not "not bookmarked". A failed load must reset
57 /// the state to unknown rather than leaving a stale prior value in
58 /// place — this proves the reset by establishing a known `true`
59 /// first, then failing a second load.
5960 @Test func aFailedListingLeavesTheStateUnknown() async throws {
6061 let (client, stub) = try makeClient()
62 stub.enqueue(.init(status: 200, json: bookmarks))
63 let model = RepoActionsViewModel(client: client, repoPath: "krz/gitbay")
64 await model.loadBookmarkState()
65 #expect(model.isBookmarked == true)
66
6167 stub.enqueue(.init(status: 200, json: """
6268 {"protocol_version":1,"error":"denied","exit_code":4}
6369 """))
64 let model = RepoActionsViewModel(client: client, repoPath: "krz/gitbay")
6570 await model.loadBookmarkState()
6671
6772 #expect(model.isBookmarked == nil)