Commit b4a6bd651f
Unsigned
Layout: unified · split
gitbay/Views/Repos/RepoView.swift +140 −60
| @@ -20,13 +20,15 @@ struct RepoView: View { | |||
| 20 | @State private var confirmingFork = false | 20 | @State private var confirmingFork = false |
| 21 | @State private var forkName = "" | 21 | @State private var forkName = "" |
| 22 | @State private var forkDestination: RepoRoute? | 22 | @State private var forkDestination: RepoRoute? |
| 23 | @State private var showingMore = false | 23 | @Environment(\.dynamicTypeSize) private var typeSize |
| 24 | @ScaledMetric(relativeTo: .title3) private var statValueHeight: CGFloat = 26 | ||
| 24 | @State private var destination: Destination? | 25 | @State private var destination: Destination? |
| 25 | 26 | ||
| 26 | /// Pushed from buttons rather than NavigationLinks: several share a | 27 | /// Pushed from buttons rather than NavigationLinks: several share a |
| 27 | /// list row, and a link there claims the whole row and adds a chevron. | 28 | /// list row, and a link there claims the whole row and adds a chevron. |
| 28 | private enum Destination: Hashable { | 29 | private enum Destination: Hashable { |
| 29 | case files, mergeRequests, issues, builds, owner, settings | 30 | case files, mergeRequests, issues, builds, owner, settings |
| 31 | case history, refs, milestones, labels, releases, wiki, grep | ||
| 30 | } | 32 | } |
| 31 | 33 | ||
| 32 | var body: some View { | 34 | var body: some View { |
| @@ -50,47 +52,13 @@ struct RepoView: View { | |||
| 50 | header(detail) | 52 | header(detail) |
| 51 | 53 | ||
| 52 | Section { | 54 | Section { |
| 53 | Grid(horizontalSpacing: 0, verticalSpacing: 0) { | 55 | statStrip |
| 54 | GridRow { | 56 | .listRowInsets(EdgeInsets()) |
| 55 | tile("Files", systemImage: "folder", id: "files", to: .files) | ||
| 56 | tile("Merge requests", systemImage: "arrow.triangle.merge", id: "merge-requests", | ||
| 57 | to: .mergeRequests, count: model.openMRs) | ||
| 58 | } | ||
| 59 | GridRow { | ||
| 60 | tile("Issues", systemImage: "smallcircle.filled.circle", id: "issues", | ||
| 61 | to: .issues, count: model.openIssues) | ||
| 62 | tile("Builds", systemImage: "hammer", id: "builds", | ||
| 63 | to: .builds, build: model.latestBuild) | ||
| 64 | } | ||
| 65 | } | ||
| 66 | .listRowInsets(EdgeInsets()) | ||
| 67 | } | 57 | } |
| 68 | 58 | ||
| 69 | Section { | 59 | Section("More") { |
| 70 | DisclosureGroup("More", isExpanded: $showingMore) { | 60 | morePills |
| 71 | NavigationLink(value: RepoRoute.log(repo: path, ref: nil, path: nil)) { | 61 | .padding(.vertical, 4) |
| 72 | Label("History", systemImage: "clock") | ||
| 73 | } | ||
| 74 | NavigationLink(value: RepoRoute.refs(repo: path)) { | ||
| 75 | Label("Branches & Tags", systemImage: "arrow.triangle.branch") | ||
| 76 | } | ||
| 77 | NavigationLink(value: RepoRoute.milestones(repo: path)) { | ||
| 78 | Label("Milestones", systemImage: "flag") | ||
| 79 | } | ||
| 80 | NavigationLink(value: RepoRoute.labels(repo: path)) { | ||
| 81 | Label("Labels", systemImage: "tag") | ||
| 82 | } | ||
| 83 | NavigationLink(value: ReleaseRoute.list(repo: path)) { | ||
| 84 | Label("Releases", systemImage: "shippingbox") | ||
| 85 | } | ||
| 86 | NavigationLink(value: RepoRoute.wiki(repo: path)) { | ||
| 87 | Label("Wiki", systemImage: "book") | ||
| 88 | } | ||
| 89 | NavigationLink(value: RepoRoute.grep(repo: path)) { | ||
| 90 | Label("Search in Files", systemImage: "text.magnifyingglass") | ||
| 91 | } | ||
| 92 | } | ||
| 93 | .accessibilityIdentifier("repo-more") | ||
| 94 | } | 62 | } |
| 95 | 63 | ||
| 96 | if let readme = model.readme { | 64 | if let readme = model.readme { |
| @@ -125,6 +93,13 @@ struct RepoView: View { | |||
| 125 | case .builds: BuildListView(client: client, repo: path) | 93 | case .builds: BuildListView(client: client, repo: path) |
| 126 | case .owner: ProfileView(client: client, name: owner) | 94 | case .owner: ProfileView(client: client, name: owner) |
| 127 | case .settings: RepoSettingsView(client: client, repo: path) | 95 | case .settings: RepoSettingsView(client: client, repo: path) |
| 96 | case .history: LogView(client: client, repo: path, ref: nil, path: nil) | ||
| 97 | case .refs: RefsView(client: client, repo: path) | ||
| 98 | case .milestones: MilestoneListView(client: client, repo: path) | ||
| 99 | case .labels: LabelListView(client: client, repo: path) | ||
| 100 | case .releases: ReleaseListView(client: client, repo: path) | ||
| 101 | case .wiki: WikiView(client: client, repo: path) | ||
| 102 | case .grep: GrepView(client: client, repo: path) | ||
| 128 | } | 103 | } |
| 129 | } | 104 | } |
| 130 | .alert("Fork \(path)", isPresented: $confirmingFork) { | 105 | .alert("Fork \(path)", isPresented: $confirmingFork) { |
| @@ -312,36 +287,141 @@ struct RepoView: View { | |||
| 312 | 287 | ||
| 313 | private var owner: String { String(path.split(separator: "/").first ?? "") } | 288 | private var owner: String { String(path.split(separator: "/").first ?? "") } |
| 314 | 289 | ||
| 315 | private func tile( | 290 | /// Files, merge requests, issues and builds in one row; two rows at |
| 316 | _ title: String, systemImage: String, id: String, to target: Destination, | 291 | /// accessibility text sizes. |
| 317 | count: String? = nil, build: Build? = nil | 292 | private var statStrip: some View { |
| 293 | let columns = typeSize.isAccessibilitySize ? 2 : 4 | ||
| 294 | return Grid(horizontalSpacing: 0, verticalSpacing: 0) { | ||
| 295 | ForEach(0..<(4 / columns), id: \.self) { row in | ||
| 296 | GridRow { | ||
| 297 | ForEach(0..<columns, id: \.self) { column in | ||
| 298 | stat(row * columns + column, first: column == 0) | ||
| 299 | .overlay(alignment: .top) { | ||
| 300 | if row > 0 { Rectangle().fill(Color.gbLine).frame(height: 0.5) } | ||
| 301 | } | ||
| 302 | } | ||
| 303 | } | ||
| 304 | } | ||
| 305 | } | ||
| 306 | } | ||
| 307 | |||
| 308 | @ViewBuilder | ||
| 309 | private func stat(_ index: Int, first: Bool) -> some View { | ||
| 310 | switch index { | ||
| 311 | case 0: | ||
| 312 | statCell("Files", label: "Files", id: "files", to: .files, first: first) { | ||
| 313 | Image(systemName: "folder").foregroundStyle(Color.gbAccent) | ||
| 314 | } | ||
| 315 | case 1: | ||
| 316 | statCell("MRs", label: "Merge requests", value: model.openMRs.map { "\($0) open" }, | ||
| 317 | id: "merge-requests", to: .mergeRequests, first: first) { | ||
| 318 | count(model.openMRs, systemImage: "arrow.triangle.merge") | ||
| 319 | } | ||
| 320 | case 2: | ||
| 321 | statCell("Issues", label: "Issues", value: model.openIssues.map { "\($0) open" }, | ||
| 322 | id: "issues", to: .issues, first: first) { | ||
| 323 | count(model.openIssues, systemImage: "smallcircle.filled.circle") | ||
| 324 | } | ||
| 325 | default: | ||
| 326 | statCell("Builds", label: "Builds", value: model.latestBuild?.status, | ||
| 327 | id: "builds", to: .builds, first: first) { | ||
| 328 | if let build = model.latestBuild { | ||
| 329 | Image(systemName: build.statusIcon).foregroundStyle(build.statusColor) | ||
| 330 | } else { | ||
| 331 | Image(systemName: "hammer").foregroundStyle(Color.gbAccent) | ||
| 332 | } | ||
| 333 | } | ||
| 334 | } | ||
| 335 | } | ||
| 336 | |||
| 337 | /// The count when it loaded, otherwise the icon. | ||
| 338 | @ViewBuilder | ||
| 339 | private func count(_ count: String?, systemImage: String) -> some View { | ||
| 340 | if let count { | ||
| 341 | Text(count).fontWeight(.medium).foregroundStyle(Color.gbAccent) | ||
| 342 | } else { | ||
| 343 | Image(systemName: systemImage).foregroundStyle(Color.gbAccent) | ||
| 344 | } | ||
| 345 | } | ||
| 346 | |||
| 347 | private func statCell( | ||
| 348 | _ title: String, label: String, value: String? = nil, id: String, | ||
| 349 | to target: Destination, first: Bool, @ViewBuilder top: () -> some View | ||
| 318 | ) -> some View { | 350 | ) -> some View { |
| 319 | Button { destination = target } label: { | 351 | Button { destination = target } label: { |
| 320 | HStack(spacing: 8) { | 352 | VStack(spacing: 2) { |
| 321 | Image(systemName: systemImage) | 353 | top() |
| 322 | .foregroundStyle(Color.gbAccent) | 354 | .font(.gbSans(.title3)) |
| 323 | .frame(width: 24) | 355 | .frame(height: statValueHeight) |
| 324 | Text(title) | 356 | Text(title) |
| 357 | .font(.gbSans(.caption)) | ||
| 325 | .lineLimit(1) | 358 | .lineLimit(1) |
| 326 | .minimumScaleFactor(0.8) | 359 | } |
| 327 | Spacer(minLength: 4) | 360 | .padding(.vertical, 10) |
| 328 | if let count { | 361 | .frame(maxWidth: .infinity) |
| 329 | Text(count) | 362 | .contentShape(Rectangle()) |
| 330 | .font(.gbSans(.subheadline)) | 363 | } |
| 331 | .foregroundStyle(.secondary) | 364 | .buttonStyle(.plain) |
| 365 | .overlay(alignment: .leading) { | ||
| 366 | if !first { Rectangle().fill(Color.gbLine).frame(width: 0.5) } | ||
| 367 | } | ||
| 368 | .accessibilityElement(children: .ignore) | ||
| 369 | .accessibilityLabel(label) | ||
| 370 | .accessibilityValue(value ?? "") | ||
| 371 | .accessibilityAddTraits(.isButton) | ||
| 372 | .accessibilityIdentifier("repo-link-\(id)") | ||
| 373 | } | ||
| 374 | |||
| 375 | /// The rest of the repository, always visible: two columns, one at | ||
| 376 | /// accessibility text sizes. | ||
| 377 | @ViewBuilder | ||
| 378 | private var morePills: some View { | ||
| 379 | let search = pill("Search in Files", systemImage: "text.magnifyingglass", to: .grep) | ||
| 380 | if typeSize.isAccessibilitySize { | ||
| 381 | VStack(spacing: 8) { | ||
| 382 | pill("History", systemImage: "clock", to: .history) | ||
| 383 | pill("Branches & Tags", systemImage: "arrow.triangle.branch", to: .refs) | ||
| 384 | pill("Milestones", systemImage: "flag", to: .milestones) | ||
| 385 | pill("Labels", systemImage: "tag", to: .labels) | ||
| 386 | pill("Releases", systemImage: "shippingbox", to: .releases) | ||
| 387 | pill("Wiki", systemImage: "book", to: .wiki) | ||
| 388 | search | ||
| 389 | } | ||
| 390 | } else { | ||
| 391 | Grid(horizontalSpacing: 8, verticalSpacing: 8) { | ||
| 392 | GridRow { | ||
| 393 | pill("History", systemImage: "clock", to: .history) | ||
| 394 | pill("Branches & Tags", systemImage: "arrow.triangle.branch", to: .refs) | ||
| 395 | } | ||
| 396 | GridRow { | ||
| 397 | pill("Milestones", systemImage: "flag", to: .milestones) | ||
| 398 | pill("Labels", systemImage: "tag", to: .labels) | ||
| 399 | } | ||
| 400 | GridRow { | ||
| 401 | pill("Releases", systemImage: "shippingbox", to: .releases) | ||
| 402 | pill("Wiki", systemImage: "book", to: .wiki) | ||
| 332 | } | 403 | } |
| 333 | if let build { | 404 | GridRow { |
| 334 | Image(systemName: build.statusIcon) | 405 | search.gridCellColumns(2) |
| 335 | .foregroundStyle(build.statusColor) | ||
| 336 | .accessibilityLabel(build.status) | ||
| 337 | } | 406 | } |
| 338 | } | 407 | } |
| 339 | .padding(.horizontal, 14) | 408 | } |
| 340 | .padding(.vertical, 12) | 409 | } |
| 410 | |||
| 411 | private func pill(_ title: String, systemImage: String, to target: Destination) -> some View { | ||
| 412 | Button { destination = target } label: { | ||
| 413 | Label { | ||
| 414 | Text(title).lineLimit(1).minimumScaleFactor(0.8) | ||
| 415 | } icon: { | ||
| 416 | Image(systemName: systemImage).foregroundStyle(Color.gbAccent) | ||
| 417 | } | ||
| 418 | .font(.gbSans(.subheadline)) | ||
| 419 | .padding(.horizontal, 10) | ||
| 420 | .padding(.vertical, 8) | ||
| 341 | .frame(maxWidth: .infinity, alignment: .leading) | 421 | .frame(maxWidth: .infinity, alignment: .leading) |
| 422 | .overlay(RoundedRectangle(cornerRadius: 2).stroke(Color.gbLine, lineWidth: 0.5)) | ||
| 342 | .contentShape(Rectangle()) | 423 | .contentShape(Rectangle()) |
| 343 | } | 424 | } |
| 344 | .buttonStyle(.plain) | 425 | .buttonStyle(.plain) |
| 345 | .accessibilityIdentifier("repo-link-\(id)") | ||
| 346 | } | 426 | } |
| 347 | } | 427 | } |
gitbayUITests/LiveSmokeUITests.swift +2 −7
| @@ -447,14 +447,9 @@ final class LiveSmokeUITests: XCTestCase { | |||
| 447 | app.descendants(matching: .any).matching(identifier: "repo-link-\(id)").firstMatch | 447 | app.descendants(matching: .any).matching(identifier: "repo-link-\(id)").firstMatch |
| 448 | } | 448 | } |
| 449 | 449 | ||
| 450 | /// A link in the repo screen's More section, above the README. | 450 | /// A button in the repo screen's More section, above the README. |
| 451 | /// Expands the section when it is still collapsed. | ||
| 452 | private func moreLink(_ title: String) -> XCUIElement { | 451 | private func moreLink(_ title: String) -> XCUIElement { |
| 453 | let link = app.staticTexts[title].firstMatch | 452 | let link = app.buttons[title].firstMatch |
| 454 | if link.exists { return link } | ||
| 455 | let more = app.descendants(matching: .any).matching(identifier: "repo-more").firstMatch | ||
| 456 | XCTAssertTrue(scrollTo(more, swipes: 20), "More section not reachable") | ||
| 457 | more.tap() | ||
| 458 | XCTAssertTrue(scrollTo(link), "\(title) not under More") | 453 | XCTAssertTrue(scrollTo(link), "\(title) not under More") |
| 459 | return link | 454 | return link |
| 460 | } | 455 | } |