Commit 0a519cfa8f
Verified · cmc
Layout: unified · split
gitbay/Discovery/ProfileEdit.swift +4 −17
| @@ -6,6 +6,9 @@ import Foundation | ||
| 6 | 6 | /// the current profile, so every other field is also sent every time, |
| 7 | 7 | /// including empty ones: `--description ""` clears, omitting the flag |
| 8 | 8 | /// would leave the old value alone. |
| 9 | /// | |
| 10 | /// The about text is not here. It is a file in `<owner>/.gitbay`, written | |
| 11 | /// through the file editor like any other file. | |
| 9 | 12 | nonisolated struct ProfileEdit: Equatable, Sendable { |
| 10 | 13 | |
| 11 | 14 | nonisolated struct Link: Equatable, Sendable, Identifiable { |
| @@ -16,27 +19,17 @@ nonisolated struct ProfileEdit: Equatable, Sendable { | ||
| 16 | 19 | |
| 17 | 20 | var description: String |
| 18 | 21 | var website: String |
| 19 | var about: String | |
| 20 | var aboutFormat: String | |
| 21 | 22 | var links: [Link] |
| 22 | 23 | |
| 23 | init( | |
| 24 | description: String = "", website: String = "", | |
| 25 | about: String = "", aboutFormat: String = "md", | |
| 26 | links: [Link] = [] | |
| 27 | ) { | |
| 24 | init(description: String = "", website: String = "", links: [Link] = []) { | |
| 28 | 25 | self.description = description |
| 29 | 26 | self.website = website |
| 30 | self.about = about | |
| 31 | self.aboutFormat = aboutFormat | |
| 32 | 27 | self.links = links |
| 33 | 28 | } |
| 34 | 29 | |
| 35 | 30 | init(from profile: ProfileViewModel.Profile) { |
| 36 | 31 | description = profile.description ?? "" |
| 37 | 32 | website = profile.website ?? "" |
| 38 | about = profile.about ?? "" | |
| 39 | aboutFormat = profile.aboutFormat ?? "md" | |
| 40 | 33 | links = (profile.links ?? []).map { Link(label: $0.label ?? "", url: $0.url) } |
| 41 | 34 | } |
| 42 | 35 | |
| @@ -56,7 +49,6 @@ nonisolated struct ProfileEdit: Equatable, Sendable { | ||
| 56 | 49 | return nil |
| 57 | 50 | } |
| 58 | 51 | |
| 59 | /// Everything except the about text, which travels over stdin. | |
| 60 | 52 | func flags() -> [String] { |
| 61 | 53 | var flags = ["--description", description, "--website", website] |
| 62 | 54 | if links.isEmpty { |
| @@ -67,11 +59,6 @@ nonisolated struct ProfileEdit: Equatable, Sendable { | ||
| 67 | 59 | flags += ["--link", label.isEmpty ? link.url : "\(label)|\(link.url)"] |
| 68 | 60 | } |
| 69 | 61 | } |
| 70 | flags += ["--about-format", aboutFormat, "--file", "-"] | |
| 71 | 62 | return flags |
| 72 | 63 | } |
| 73 | ||
| 74 | /// The about text, sent over stdin — never in argv, since it is | |
| 75 | /// long-form and may hold newlines. | |
| 76 | var aboutStdin: String { about } | |
| 77 | 64 | } |
gitbay/Discovery/ProfileViewModel.swift +2 −3
| @@ -134,12 +134,11 @@ final class ProfileViewModel { | ||
| 134 | 134 | } |
| 135 | 135 | |
| 136 | 136 | /// `profile set` for the caller's own profile, `org profile <org>` for |
| 137 | /// an organization; both take the same flags. The about text travels | |
| 138 | /// over stdin, never argv, since it is long-form and may carry newlines. | |
| 137 | /// an organization; both take the same flags. | |
| 139 | 138 | func saveProfile(_ edit: ProfileEdit) async -> Bool { |
| 140 | 139 | guard edit.validationError == nil else { return false } |
| 141 | 140 | let command = state.value?.isOrg == true ? ["org", "profile", name] : ["profile", "set"] |
| 142 | await perform(command + edit.flags(), stdin: edit.aboutStdin) | |
| 141 | await perform(command + edit.flags()) | |
| 143 | 142 | return actionError == nil |
| 144 | 143 | } |
| 145 | 144 | |
gitbay/Views/Discovery/ProfileEditSheet.swift +4 −22
| @@ -1,10 +1,9 @@ | ||
| 1 | 1 | import SwiftUI |
| 2 | 2 | |
| 3 | /// `profile set` — always the caller's own. Description and website are | |
| 4 | /// single lines; about is long-form prose in whichever format the | |
| 5 | /// profile already carries, since switching formats reinterprets text | |
| 6 | /// that was already written; links are capped at five, matching the | |
| 7 | /// server. | |
| 3 | /// `profile set`, or `org profile` for an organization. Description and | |
| 4 | /// website are single lines; links are capped at five, matching the | |
| 5 | /// server. The about text is a file and is edited from the profile | |
| 6 | /// screen, not here. | |
| 8 | 7 | struct ProfileEditSheet: View { |
| 9 | 8 | |
| 10 | 9 | let model: ProfileViewModel |
| @@ -35,23 +34,6 @@ struct ProfileEditSheet: View { | ||
| 35 | 34 | .textInputAutocapitalization(.never) |
| 36 | 35 | .accessibilityIdentifier("profile-edit-website") |
| 37 | 36 | } |
| 38 | Section { | |
| 39 | Picker("Format", selection: $edit.aboutFormat) { | |
| 40 | Text("Markdown").tag("md") | |
| 41 | Text("Org").tag("org") | |
| 42 | } | |
| 43 | .pickerStyle(.segmented) | |
| 44 | .accessibilityIdentifier("profile-edit-format") | |
| 45 | TextEditor(text: $edit.about) | |
| 46 | .font(.gbMono(.caption)) | |
| 47 | .frame(minHeight: 160) | |
| 48 | .autocorrectionDisabled() | |
| 49 | .accessibilityIdentifier("profile-edit-about") | |
| 50 | } header: { | |
| 51 | Text("About") | |
| 52 | } footer: { | |
| 53 | Text("Changing the format reinterprets text that is already written.") | |
| 54 | } | |
| 55 | 37 | linksSection |
| 56 | 38 | if let error = localError ?? model.actionError { |
| 57 | 39 | Section { |
gitbayTests/ProfileEditTests.swift +12 −41
| @@ -6,11 +6,9 @@ struct ProfileEditFlagTests { | ||
| 6 | 6 | |
| 7 | 7 | private func edit( |
| 8 | 8 | description: String = "", website: String = "", |
| 9 | about: String = "", aboutFormat: String = "md", | |
| 10 | 9 | links: [ProfileEdit.Link] = [] |
| 11 | 10 | ) -> ProfileEdit { |
| 12 | ProfileEdit(description: description, website: website, | |
| 13 | about: about, aboutFormat: aboutFormat, links: links) | |
| 11 | ProfileEdit(description: description, website: website, links: links) | |
| 14 | 12 | } |
| 15 | 13 | |
| 16 | 14 | @Test func everyFieldSendsItsFlag() { |
| @@ -59,19 +57,14 @@ struct ProfileEditFlagTests { | ||
| 59 | 57 | #expect(flags.contains("|https://a.test") == false) |
| 60 | 58 | } |
| 61 | 59 | |
| 62 | /// Long-form text with newlines belongs in stdin, not argv. | |
| 63 | @Test func theAboutTextGoesToStdinNotArgv() { | |
| 64 | let e = edit(about: "line one\n\nline two", aboutFormat: "org") | |
| 65 | #expect(e.flags().contains { $0.contains("line one") } == false) | |
| 66 | #expect(e.flags().contains("--file")) | |
| 67 | #expect(e.flags().contains("-")) | |
| 68 | #expect(e.aboutStdin == "line one\n\nline two") | |
| 69 | } | |
| 70 | ||
| 71 | @Test func theAboutFormatAccompaniesTheText() { | |
| 72 | let flags = edit(about: "x", aboutFormat: "org").flags() | |
| 73 | let i = try! #require(flags.firstIndex(of: "--about-format")) | |
| 74 | #expect(flags[i + 1] == "org") | |
| 60 | /// Since gitbay v1.31.0 the about text is a file in <owner>/.gitbay, | |
| 61 | /// and `profile set` refuses any of these as an unknown flag (exit 2). | |
| 62 | @Test func noAboutFlagIsSent() { | |
| 63 | let flags = edit(description: "hi", website: "https://x.test", | |
| 64 | links: [.init(label: "a", url: "https://a.test")]).flags() | |
| 65 | #expect(!flags.contains("--about")) | |
| 66 | #expect(!flags.contains("--about-format")) | |
| 67 | #expect(!flags.contains("--file")) | |
| 75 | 68 | } |
| 76 | 69 | |
| 77 | 70 | @Test func moreThanFiveLinksIsInvalid() { |
| @@ -103,36 +96,17 @@ struct ProfileEditFromProfileTests { | ||
| 103 | 96 | "repos":[],"activity_total":0} |
| 104 | 97 | """ |
| 105 | 98 | |
| 106 | /// The real profile on gitbay.org is org, with tables and links that | |
| 107 | /// render as flat text under markdown — the stored format must round | |
| 108 | /// trip, not default away from what the server holds. | |
| 109 | @Test func seedsFromAnOrgProfile() throws { | |
| 99 | @Test func seedsFromTheProfile() throws { | |
| 110 | 100 | let data = try #require(orgProfileJSON.data(using: .utf8)) |
| 111 | 101 | let profile = try JSONDecoder().decode(ProfileViewModel.Profile.self, from: data) |
| 112 | 102 | |
| 113 | 103 | let edit = ProfileEdit(from: profile) |
| 114 | 104 | |
| 115 | #expect(edit.aboutFormat == "org") | |
| 116 | 105 | #expect(edit.description == "about me") |
| 117 | 106 | #expect(edit.website == "https://cleberg.net") |
| 118 | #expect(edit.about == "* heading\n\n| a | b |") | |
| 119 | 107 | #expect(edit.links.map(\.url) == ["https://a.test", "https://b.test"]) |
| 120 | 108 | #expect(edit.links.map(\.label) == ["", "blog"]) |
| 121 | 109 | } |
| 122 | ||
| 123 | /// Absent `about_format`, seeding falls back to markdown. | |
| 124 | @Test func seedingFallsBackToMarkdownWhenFormatIsAbsent() throws { | |
| 125 | let json = """ | |
| 126 | {"name":"cmc","kind":"user",\ | |
| 127 | "description":"hi","repos":[],"activity_total":0} | |
| 128 | """ | |
| 129 | let data = try #require(json.data(using: .utf8)) | |
| 130 | let profile = try JSONDecoder().decode(ProfileViewModel.Profile.self, from: data) | |
| 131 | ||
| 132 | let edit = ProfileEdit(from: profile) | |
| 133 | ||
| 134 | #expect(edit.aboutFormat == "md") | |
| 135 | } | |
| 136 | 110 | } |
| 137 | 111 | |
| 138 | 112 | private func makeClient() throws -> (GitbayClient, StubProtocol.Box) { |
| @@ -170,14 +144,13 @@ struct ProfileSaveTests { | ||
| 170 | 144 | /// The write, then the reload `profile set` triggers — one POST, one |
| 171 | 145 | /// GET. Both need a stub or the flow ends up looking like a refusal |
| 172 | 146 | /// even when the write itself succeeded. |
| 173 | @Test func savesTheFullArgvWithAboutOnlyInStdin() async throws { | |
| 147 | @Test func savesScalarsAndLinksWithNoStdin() async throws { | |
| 174 | 148 | let (client, stub) = try makeClient() |
| 175 | 149 | stub.enqueue(.init(status: 200, json: okJSON, match: "cmd")) |
| 176 | 150 | stub.enqueue(.init(status: 200, json: profileJSON, match: "argv=show")) |
| 177 | 151 | let model = ProfileViewModel(client: client, name: "cmc") |
| 178 | 152 | let edit = ProfileEdit( |
| 179 | 153 | description: "hi", website: "https://cleberg.net", |
| 180 | about: "long form\n\nprose", aboutFormat: "org", | |
| 181 | 154 | links: [.init(label: "blog", url: "https://b.test")] |
| 182 | 155 | ) |
| 183 | 156 | |
| @@ -190,10 +163,8 @@ struct ProfileSaveTests { | ||
| 190 | 163 | "profile", "set", |
| 191 | 164 | "--description", "hi", "--website", "https://cleberg.net", |
| 192 | 165 | "--link", "blog|https://b.test", |
| 193 | "--about-format", "org", "--file", "-", | |
| 194 | 166 | ]) |
| 195 | #expect(stdin == "long form\n\nprose") | |
| 196 | #expect(argv.contains { $0.contains("long form") } == false) | |
| 167 | #expect(stdin == nil) | |
| 197 | 168 | } |
| 198 | 169 | |
| 199 | 170 | /// An org's profile is written by `org profile <org>`, with the same |