Restructure PR detail: remove Commits tab, bottom icon tab bar, toolbar actions, markdown overview; bump to 1.9
Build iOS App / Build .ipa and publish source (push) Successful in 21s
Build iOS App / Build .ipa and publish source (push) Successful in 21s
- Remove the Commits tab (not useful). - Move the section switcher to a bottom tab bar with icons (Overview / Conversation / Files / Checks) instead of a top segmented control. - Move Review (menu) and Merge into the navigation toolbar, removing the bottom action bar. - Add an Overview tab that renders the PR body as markdown via SwiftUI's built-in AttributedString(markdown:) (no third-party dependency). - Bump to 1.9.
This commit is contained in:
1 parent
de9729b898
commit
fce0385708
3 files changed
+100
-66
No files matched your search
@@ -187,7 +187,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 9;
|
||||
CURRENT_PROJECT_VERSION = 10;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "GiteaPR";
|
||||
@@ -197,7 +197,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.8;
|
||||
MARKETING_VERSION = 1.9;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.bueso.giteapr;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
@@ -211,7 +211,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 9;
|
||||
CURRENT_PROJECT_VERSION = 10;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "GiteaPR";
|
||||
@@ -221,7 +221,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.8;
|
||||
MARKETING_VERSION = 1.9;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.bueso.giteapr;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
|
||||
@@ -2,8 +2,7 @@ import SwiftUI
|
||||
|
||||
struct PRDetailView: View {
|
||||
@StateObject private var vm: PRDetailViewModel
|
||||
@State private var selectedTab: DetailTab = .conversation
|
||||
@State private var showActionSheet = false
|
||||
@State private var selectedTab: DetailTab = .overview
|
||||
@State private var showCommentSheet = false
|
||||
@State private var showMergeSheet = false
|
||||
@State private var showCloseConfirm = false
|
||||
@@ -11,11 +10,21 @@ struct PRDetailView: View {
|
||||
@State private var requestChangesMode = false
|
||||
|
||||
enum DetailTab: String, CaseIterable, Identifiable {
|
||||
case overview = "Overview"
|
||||
case conversation = "Conversation"
|
||||
case files = "Files"
|
||||
case commits = "Commits"
|
||||
case checks = "Checks"
|
||||
|
||||
var id: String { rawValue }
|
||||
|
||||
var icon: String {
|
||||
switch self {
|
||||
case .overview: return "doc.text"
|
||||
case .conversation: return "bubble.left.and.bubble.right"
|
||||
case .files: return "doc.plaintext"
|
||||
case .checks: return "checkmark.circle"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init(repo: Repository, prNumber: Int) {
|
||||
@@ -47,17 +56,6 @@ struct PRDetailView: View {
|
||||
Button("OK", role: .cancel) {}
|
||||
} message: { Text(vm.errorMessage ?? "") }
|
||||
.overlay { if vm.busyMessage != nil { busyOverlay } }
|
||||
.confirmationDialog("Actions", isPresented: $showActionSheet, titleVisibility: .visible) {
|
||||
Button("Comment") { showCommentSheet = true }
|
||||
Button("Approve") { Task { await vm.submitReview(state: "approved", comment: nil) } }
|
||||
Button("Request changes") { requestChangesMode = true; showCommentSheet = true }
|
||||
Button("Merge") { showMergeSheet = true }
|
||||
if prIsOpen {
|
||||
Button("Close", role: .destructive) { showCloseConfirm = true }
|
||||
} else {
|
||||
Button("Reopen") { Task { await vm.setState("open") } }
|
||||
}
|
||||
}
|
||||
.confirmationDialog("Close this PR?", isPresented: $showCloseConfirm, titleVisibility: .visible) {
|
||||
Button("Close", role: .destructive) { Task { await vm.setState("closed") } }
|
||||
Button("Cancel", role: .cancel) {}
|
||||
@@ -89,28 +87,24 @@ struct PRDetailView: View {
|
||||
private func content(_ pr: PullRequest) -> some View {
|
||||
VStack(spacing: 0) {
|
||||
header(pr)
|
||||
Picker("Tab", selection: $selectedTab) {
|
||||
ForEach(DetailTab.allCases) { t in Text(t.rawValue).tag(t) }
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
.padding(.horizontal)
|
||||
.padding(.bottom, 8)
|
||||
|
||||
ScrollView {
|
||||
switch selectedTab {
|
||||
case .overview: OverviewTab(pr: pr)
|
||||
case .conversation: ConversationTab(vm: vm)
|
||||
case .files: FilesTab(vm: vm)
|
||||
case .commits: CommitsTab(commits: vm.commits)
|
||||
case .checks: ChecksTab(vm: vm, onRerun: { showRerunConfirm = $0 })
|
||||
}
|
||||
}
|
||||
|
||||
if prIsOpen {
|
||||
actionBar(pr)
|
||||
}
|
||||
bottomTabBar
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .primaryAction) { reloadButton }
|
||||
if prIsOpen {
|
||||
ToolbarItem(placement: .primaryAction) { mergeButton }
|
||||
ToolbarItem(placement: .primaryAction) { reviewMenu }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,36 +125,60 @@ struct PRDetailView: View {
|
||||
Text("Draft").font(.caption2).foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
if let body = pr.body, !body.isEmpty {
|
||||
Text(body)
|
||||
.font(.callout)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(Color(.secondarySystemBackground))
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func actionBar(_ pr: PullRequest) -> some View {
|
||||
/// Bottom tab bar: icon-only navigation between the PR's sections.
|
||||
private var bottomTabBar: some View {
|
||||
HStack {
|
||||
Button { showActionSheet = true } label: {
|
||||
Label("Review", systemImage: "checkmark.bubble")
|
||||
ForEach(DetailTab.allCases) { tab in
|
||||
Button {
|
||||
selectedTab = tab
|
||||
} label: {
|
||||
VStack(spacing: 2) {
|
||||
Image(systemName: tab.icon)
|
||||
.font(.system(size: 20))
|
||||
Text(tab.rawValue)
|
||||
.font(.caption2)
|
||||
}
|
||||
.foregroundColor(selectedTab == tab ? .accentColor : .secondary)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
|
||||
Button("Merge") { showMergeSheet = true }
|
||||
.buttonStyle(.bordered)
|
||||
.disabled(vm.ciStatus == .failure &&
|
||||
SettingsStore.shared.mergeGateMode == .block)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
.padding(.vertical, 6)
|
||||
.background(.bar)
|
||||
}
|
||||
|
||||
private var reviewMenu: some View {
|
||||
Menu {
|
||||
Button("Comment") { requestChangesMode = false; showCommentSheet = true }
|
||||
Button("Approve") { Task { await vm.submitReview(state: "approved", comment: nil) } }
|
||||
Button("Request changes") { requestChangesMode = true; showCommentSheet = true }
|
||||
Divider()
|
||||
if prIsOpen {
|
||||
Button("Close", role: .destructive) { showCloseConfirm = true }
|
||||
} else {
|
||||
Button("Reopen") { Task { await vm.setState("open") } }
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "checkmark.bubble")
|
||||
}
|
||||
}
|
||||
|
||||
private var mergeButton: some View {
|
||||
Button {
|
||||
showMergeSheet = true
|
||||
} label: {
|
||||
Image(systemName: "arrow.down.circle")
|
||||
}
|
||||
.disabled(vm.ciStatus == .failure &&
|
||||
SettingsStore.shared.mergeGateMode == .block)
|
||||
}
|
||||
|
||||
private var prIsOpen: Bool {
|
||||
(vm.pr?.state.lowercased() ?? "") == "open"
|
||||
}
|
||||
|
||||
+36
-20
@@ -142,28 +142,22 @@ struct FilesTab: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Commits
|
||||
// MARK: - Overview
|
||||
|
||||
/// Renders the PR body as markdown. Uses SwiftUI's built-in
|
||||
/// `AttributedString(markdown:)` so headings, bold, italic, links and inline
|
||||
/// code render without a third-party dependency.
|
||||
struct OverviewTab: View {
|
||||
let pr: PullRequest
|
||||
|
||||
struct CommitsTab: View {
|
||||
let commits: [PRCommit]
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
ForEach(commits) { commit in
|
||||
HStack(alignment: .top, spacing: 10) {
|
||||
Text(commit.sha ?? commit.id)
|
||||
.font(.caption2.monospaced())
|
||||
.foregroundColor(.secondary)
|
||||
.frame(width: 60, alignment: .leading)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(commit.commit?.message ?? "(no message)")
|
||||
.font(.callout)
|
||||
.lineLimit(3)
|
||||
Text(commit.author?.name ?? "")
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
Divider()
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
if let body = pr.body, !body.isEmpty {
|
||||
MarkdownText(body)
|
||||
} else {
|
||||
Text("No description.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
@@ -171,6 +165,28 @@ struct CommitsTab: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// A Text view that renders a markdown string, falling back to plain text if
|
||||
/// the markdown can't be parsed.
|
||||
struct MarkdownText: View {
|
||||
let markdown: String
|
||||
|
||||
init(_ markdown: String) {
|
||||
self.markdown = markdown
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if let attributed = try? AttributedString(markdown: markdown) {
|
||||
Text(attributed)
|
||||
.font(.callout)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
} else {
|
||||
Text(markdown)
|
||||
.font(.callout)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Checks
|
||||
|
||||
struct ChecksTab: View {
|
||||
|
||||
Reference in new issue
Block a user