Remove a release asset !67
3 files changed, +67 −0
Layout: unified · split
gitbay/Releases/ReleaseViewModels.swift +15
| @@ -100,6 +100,21 @@ final class ReleaseDetailViewModel { | |||
| 100 | } | 100 | } |
| 101 | } | 101 | } |
| 102 | 102 | ||
| 103 | /// `release asset remove <owner/name> <tag> <filename>`. | ||
| 104 | func removeAsset(named name: String) async { | ||
| 105 | working = true | ||
| 106 | actionError = nil | ||
| 107 | defer { working = false } | ||
| 108 | do { | ||
| 109 | try await client.run(["release", "asset", "remove", repoPath, tag, name]) | ||
| 110 | await load() | ||
| 111 | } catch let error as GitbayError { | ||
| 112 | actionError = error.userFacingMessage | ||
| 113 | } catch { | ||
| 114 | actionError = GitbayError.transport(error).userFacingMessage | ||
| 115 | } | ||
| 116 | } | ||
| 117 | |||
| 103 | /// `release delete <owner/name> <tag> --yes` — `--yes` is required by | 118 | /// `release delete <owner/name> <tag> --yes` — `--yes` is required by |
| 104 | /// the command, not optional; omitting it is a usage error. | 119 | /// the command, not optional; omitting it is a usage error. |
| 105 | func delete() async -> Bool { | 120 | func delete() async -> Bool { |
gitbay/Views/Releases/ReleaseView.swift +23
| @@ -7,6 +7,7 @@ struct ReleaseView: View { | |||
| 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 | 9 | @State private var confirmingDelete = false |
| 10 | @State private var removingAsset: Release.Asset? | ||
| 10 | @Environment(\.dismiss) private var dismiss | 11 | @Environment(\.dismiss) private var dismiss |
| 11 | 12 | ||
| 12 | init(client: GitbayClient, repo: String, tag: String) { | 13 | init(client: GitbayClient, repo: String, tag: String) { |
| @@ -71,6 +72,12 @@ struct ReleaseView: View { | |||
| 71 | .foregroundStyle(.secondary) | 72 | .foregroundStyle(.secondary) |
| 72 | } | 73 | } |
| 73 | } | 74 | } |
| 75 | .swipeActions { | ||
| 76 | Button("Remove", role: .destructive) { | ||
| 77 | removingAsset = asset | ||
| 78 | } | ||
| 79 | .disabled(model.working) | ||
| 80 | } | ||
| 74 | } | 81 | } |
| 75 | } | 82 | } |
| 76 | } | 83 | } |
| @@ -117,6 +124,22 @@ struct ReleaseView: View { | |||
| 117 | } message: { | 124 | } message: { |
| 118 | Text("This deletes the release's assets along with it.") | 125 | Text("This deletes the release's assets along with it.") |
| 119 | } | 126 | } |
| 127 | .confirmationDialog( | ||
| 128 | "Remove \(removingAsset?.name ?? "") from \(model.tag)?", | ||
| 129 | isPresented: Binding( | ||
| 130 | get: { removingAsset != nil }, | ||
| 131 | set: { if !$0 { removingAsset = nil } } | ||
| 132 | ), | ||
| 133 | titleVisibility: .visible | ||
| 134 | ) { | ||
| 135 | Button("Remove", role: .destructive) { | ||
| 136 | if let asset = removingAsset { | ||
| 137 | Task { await model.removeAsset(named: asset.name) } | ||
| 138 | } | ||
| 139 | removingAsset = nil | ||
| 140 | } | ||
| 141 | Button("Cancel", role: .cancel) {} | ||
| 142 | } | ||
| 120 | .sheet(isPresented: $editing) { | 143 | .sheet(isPresented: $editing) { |
| 121 | ComposeSheet( | 144 | ComposeSheet( |
| 122 | heading: "Edit \(model.tag)", | 145 | heading: "Edit \(model.tag)", |
gitbayTests/ReleaseTests.swift +29
| @@ -117,6 +117,35 @@ struct ReleaseDetailViewModelTests { | |||
| 117 | #expect(stdin == "new notes") | 117 | #expect(stdin == "new notes") |
| 118 | } | 118 | } |
| 119 | 119 | ||
| 120 | @Test func removeAssetSendsTheCommandAndReloads() async throws { | ||
| 121 | let (client, stub) = try makeClient() | ||
| 122 | stub.enqueue(.init(status: 200, json: releaseShowJSON, match: "argv=show")) | ||
| 123 | let model = ReleaseDetailViewModel(client: client, repoPath: "krz/gitbay", tag: "v1.0.0") | ||
| 124 | await model.load() | ||
| 125 | stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":{},"exit_code":0}"#, match: "cmd")) | ||
| 126 | stub.enqueue(.init(status: 200, json: releaseShowJSON, match: "argv=show")) | ||
| 127 | |||
| 128 | await model.removeAsset(named: "SHA256SUMS") | ||
| 129 | |||
| 130 | let (argv, _) = try argvOf(try #require(stub.seen.first { $0.method == "POST" })) | ||
| 131 | #expect(argv == ["release", "asset", "remove", "krz/gitbay", "v1.0.0", "SHA256SUMS"]) | ||
| 132 | #expect(stub.seen.filter { $0.method == "GET" }.count == 2) | ||
| 133 | #expect(model.actionError == nil) | ||
| 134 | } | ||
| 135 | |||
| 136 | @Test func aRefusedAssetRemovalSurfaces() async throws { | ||
| 137 | let (client, stub) = try makeClient() | ||
| 138 | stub.enqueue(.init(status: 200, json: releaseShowJSON, match: "argv=show")) | ||
| 139 | let model = ReleaseDetailViewModel(client: client, repoPath: "krz/gitbay", tag: "v1.0.0") | ||
| 140 | await model.load() | ||
| 141 | stub.enqueue(.init(status: 403, json: #"{"protocol_version":1,"error":"write access required","exit_code":4}"#, match: "cmd")) | ||
| 142 | |||
| 143 | await model.removeAsset(named: "SHA256SUMS") | ||
| 144 | |||
| 145 | #expect(model.actionError == "write access required") | ||
| 146 | #expect(model.state.value?.assets?.count == 1) | ||
| 147 | } | ||
| 148 | |||
| 120 | @Test func downloadURLsUseTheWebRouteOnTheInstance() async throws { | 149 | @Test func downloadURLsUseTheWebRouteOnTheInstance() async throws { |
| 121 | let (client, _) = try makeClient() | 150 | let (client, _) = try makeClient() |
| 122 | let model = ReleaseDetailViewModel(client: client, repoPath: "krz/gitbay", tag: "v1.0.0") | 151 | let model = ReleaseDetailViewModel(client: client, repoPath: "krz/gitbay", tag: "v1.0.0") |