Show milestone and pull request states

This commit is contained in:
Georg Bauer
2026-07-31 18:39:10 +02:00
parent acf2c839ad
commit 128be97e20
6 changed files with 148 additions and 33 deletions

View File

@@ -267,9 +267,11 @@ xcrun simctl launch booted de.rfc1437.gotcha
Open + All Milestones, refreshes the rows, returns both icons to neutral, Open + All Milestones, refreshes the rows, returns both icons to neutral,
and remains cleared after relaunch. and remains cleared after relaunch.
- [ ] Each row shows repository/number, title, author/update metadata, comment - [ ] Each row shows repository/number, title, author/update metadata, comment
count, and draft/merged state where applicable. count, and draft/merged state where applicable. Open rows show a green
icon, closed rows show a purple icon, and VoiceOver announces the state.
- [ ] Open a pull request and verify title, metadata, Markdown body, comments, - [ ] Open a pull request and verify title, metadata, Markdown body, comments,
and changed-file reference. Its file paths, statuses, left alignment, changed-file reference, and matching open/closed state icon with a
VoiceOver state announcement. Its file paths, statuses, left alignment,
separators, and disclosure indicators match the commit changed-file list. separators, and disclosure indicators match the commit changed-file list.
- [ ] Open a changed file and run the same diff checks as for a commit. - [ ] Open a changed file and run the same diff checks as for a commit.
@@ -280,7 +282,8 @@ xcrun simctl launch booted de.rfc1437.gotcha
- [ ] Toggle a repository favorite and confirm it persists after refresh and - [ ] Toggle a repository favorite and confirm it persists after refresh and
relaunch without affecting the same repository in Issues or Repos. relaunch without affecting the same repository in Issues or Repos.
- [ ] Each milestone shows its title, description, state, due date, issue - [ ] Each milestone shows its title, description, state, due date, issue
counts, and a green/amber closed/open progress bar. counts, and a green/amber closed/open progress bar. Lists and details show
a green open or purple closed icon and VoiceOver announces the state.
- [ ] Open a milestone and verify every assigned issue and pull request is - [ ] Open a milestone and verify every assigned issue and pull request is
listed; tapping either opens its normal detail. listed; tapping either opens its normal detail.
- [ ] Tap the add button. **New Milestone** has native Cancel and Save controls, - [ ] Tap the add button. **New Milestone** has native Cancel and Save controls,

View File

@@ -57,6 +57,7 @@ pub struct PullRow {
pub number: i64, pub number: i64,
pub owner: String, pub owner: String,
pub repository: String, pub repository: String,
pub state: String,
pub title: String, pub title: String,
pub summary: String, pub summary: String,
pub meta: String, pub meta: String,
@@ -118,6 +119,7 @@ pub struct IssueFilterOptions {
#[derive(Clone, uniffi::Record)] #[derive(Clone, uniffi::Record)]
pub struct MilestoneRow { pub struct MilestoneRow {
pub id: i64, pub id: i64,
pub state: String,
pub title: String, pub title: String,
pub description: String, pub description: String,
pub meta: String, pub meta: String,
@@ -156,6 +158,7 @@ pub struct PullFilterOptions {
#[derive(Clone, uniffi::Record)] #[derive(Clone, uniffi::Record)]
pub struct PullPage { pub struct PullPage {
pub title: String, pub title: String,
pub state: String,
pub meta: String, pub meta: String,
pub body: String, pub body: String,
pub files_ref: String, pub files_ref: String,
@@ -443,6 +446,7 @@ pub fn pull_rows(pulls: &[models::Issue]) -> Vec<PullRow> {
number: pull.number.unwrap_or_default(), number: pull.number.unwrap_or_default(),
owner: repository.owner.clone()?, owner: repository.owner.clone()?,
repository: repository.name.clone()?, repository: repository.name.clone()?,
state: pull.state.clone().unwrap_or_else(|| "unknown".into()),
title: format!( title: format!(
"{} #{}\n{}", "{} #{}\n{}",
repository.name.as_deref()?, repository.name.as_deref()?,
@@ -664,6 +668,7 @@ pub fn pull_page(details: PullDetails) -> PullPage {
.title .title
.clone() .clone()
.unwrap_or_else(|| "Untitled pull request".into()), .unwrap_or_else(|| "Untitled pull request".into()),
state: pull.state.clone().unwrap_or_else(|| "unknown".into()),
meta: format!( meta: format!(
"#{} · {state} · {author} · {head} → {base}\n{} files · +{} {} · {} comments", "#{} · {state} · {author} · {head} → {base}\n{} files · +{} {} · {} comments",
pull.number.unwrap_or_default(), pull.number.unwrap_or_default(),
@@ -1077,6 +1082,7 @@ fn milestone_row(milestone: &models::Milestone) -> MilestoneRow {
.unwrap_or_else(|| "no due date".into()); .unwrap_or_else(|| "no due date".into());
MilestoneRow { MilestoneRow {
id: milestone.id.unwrap_or_default(), id: milestone.id.unwrap_or_default(),
state: milestone.state.clone().unwrap_or_else(|| "unknown".into()),
title: milestone title: milestone
.title .title
.clone() .clone()
@@ -1239,6 +1245,7 @@ mod tests {
}; };
let milestone_row = milestone_rows(std::slice::from_ref(&milestone)).remove(0); let milestone_row = milestone_rows(std::slice::from_ref(&milestone)).remove(0);
assert_eq!(milestone_row.progress, 0.4); assert_eq!(milestone_row.progress, 0.4);
assert_eq!(milestone_row.state, "open");
assert_eq!(milestone_row.progress_accessibility, "2 closed, 3 open"); assert_eq!(milestone_row.progress_accessibility, "2 closed, 3 open");
let issue = models::Issue { let issue = models::Issue {
state: Some("closed".into()), state: Some("closed".into()),
@@ -1406,6 +1413,7 @@ mod tests {
issues: Vec::new(), issues: Vec::new(),
pulls: vec![models::Issue { pulls: vec![models::Issue {
number: Some(7), number: Some(7),
state: Some("closed".into()),
title: Some("Linked pull".into()), title: Some("Linked pull".into()),
repository: Some(Box::new(models::RepositoryMeta { repository: Some(Box::new(models::RepositoryMeta {
name: Some("demo".into()), name: Some("demo".into()),
@@ -1420,6 +1428,22 @@ mod tests {
assert!(page.issues.is_empty()); assert!(page.issues.is_empty());
assert_eq!(page.pulls.len(), 1); assert_eq!(page.pulls.len(), 1);
assert_eq!(page.pulls[0].number, 7); assert_eq!(page.pulls[0].number, 7);
assert_eq!(page.pulls[0].state, "closed");
}
#[test]
fn pull_page_exposes_open_closed_state() {
let page = pull_page(PullDetails {
pull: models::PullRequest {
state: Some("closed".into()),
..Default::default()
},
comments: Vec::new(),
files: Vec::new(),
has_more: false,
});
assert_eq!(page.state, "closed");
} }
#[test] #[test]

View File

@@ -2617,6 +2617,7 @@ public func FfiConverterTypeMilestonePage_lower(_ value: MilestonePage) -> RustB
public struct MilestoneRow: Equatable, Hashable { public struct MilestoneRow: Equatable, Hashable {
public var id: Int64 public var id: Int64
public var state: String
public var title: String public var title: String
public var description: String public var description: String
public var meta: String public var meta: String
@@ -2626,8 +2627,9 @@ public struct MilestoneRow: Equatable, Hashable {
// Default memberwise initializers are never public by default, so we // Default memberwise initializers are never public by default, so we
// declare one manually. // declare one manually.
public init(id: Int64, title: String, description: String, meta: String, hasIssues: Bool, progress: Double, progressAccessibility: String) { public init(id: Int64, state: String, title: String, description: String, meta: String, hasIssues: Bool, progress: Double, progressAccessibility: String) {
self.id = id self.id = id
self.state = state
self.title = title self.title = title
self.description = description self.description = description
self.meta = meta self.meta = meta
@@ -2653,6 +2655,7 @@ public struct FfiConverterTypeMilestoneRow: FfiConverterRustBuffer {
return return
try MilestoneRow( try MilestoneRow(
id: FfiConverterInt64.read(from: &buf), id: FfiConverterInt64.read(from: &buf),
state: FfiConverterString.read(from: &buf),
title: FfiConverterString.read(from: &buf), title: FfiConverterString.read(from: &buf),
description: FfiConverterString.read(from: &buf), description: FfiConverterString.read(from: &buf),
meta: FfiConverterString.read(from: &buf), meta: FfiConverterString.read(from: &buf),
@@ -2664,6 +2667,7 @@ public struct FfiConverterTypeMilestoneRow: FfiConverterRustBuffer {
public static func write(_ value: MilestoneRow, into buf: inout [UInt8]) { public static func write(_ value: MilestoneRow, into buf: inout [UInt8]) {
FfiConverterInt64.write(value.id, into: &buf) FfiConverterInt64.write(value.id, into: &buf)
FfiConverterString.write(value.state, into: &buf)
FfiConverterString.write(value.title, into: &buf) FfiConverterString.write(value.title, into: &buf)
FfiConverterString.write(value.description, into: &buf) FfiConverterString.write(value.description, into: &buf)
FfiConverterString.write(value.meta, into: &buf) FfiConverterString.write(value.meta, into: &buf)
@@ -2799,6 +2803,7 @@ public func FfiConverterTypePullListPage_lower(_ value: PullListPage) -> RustBuf
public struct PullPage: Equatable, Hashable { public struct PullPage: Equatable, Hashable {
public var title: String public var title: String
public var state: String
public var meta: String public var meta: String
public var body: String public var body: String
public var filesRef: String public var filesRef: String
@@ -2808,8 +2813,9 @@ public struct PullPage: Equatable, Hashable {
// Default memberwise initializers are never public by default, so we // Default memberwise initializers are never public by default, so we
// declare one manually. // declare one manually.
public init(title: String, meta: String, body: String, filesRef: String, files: [FileRow], comments: [CommentRow], hasMore: Bool) { public init(title: String, state: String, meta: String, body: String, filesRef: String, files: [FileRow], comments: [CommentRow], hasMore: Bool) {
self.title = title self.title = title
self.state = state
self.meta = meta self.meta = meta
self.body = body self.body = body
self.filesRef = filesRef self.filesRef = filesRef
@@ -2835,6 +2841,7 @@ public struct FfiConverterTypePullPage: FfiConverterRustBuffer {
return return
try PullPage( try PullPage(
title: FfiConverterString.read(from: &buf), title: FfiConverterString.read(from: &buf),
state: FfiConverterString.read(from: &buf),
meta: FfiConverterString.read(from: &buf), meta: FfiConverterString.read(from: &buf),
body: FfiConverterString.read(from: &buf), body: FfiConverterString.read(from: &buf),
filesRef: FfiConverterString.read(from: &buf), filesRef: FfiConverterString.read(from: &buf),
@@ -2846,6 +2853,7 @@ public struct FfiConverterTypePullPage: FfiConverterRustBuffer {
public static func write(_ value: PullPage, into buf: inout [UInt8]) { public static func write(_ value: PullPage, into buf: inout [UInt8]) {
FfiConverterString.write(value.title, into: &buf) FfiConverterString.write(value.title, into: &buf)
FfiConverterString.write(value.state, into: &buf)
FfiConverterString.write(value.meta, into: &buf) FfiConverterString.write(value.meta, into: &buf)
FfiConverterString.write(value.body, into: &buf) FfiConverterString.write(value.body, into: &buf)
FfiConverterString.write(value.filesRef, into: &buf) FfiConverterString.write(value.filesRef, into: &buf)
@@ -2875,16 +2883,18 @@ public struct PullRow: Equatable, Hashable {
public var number: Int64 public var number: Int64
public var owner: String public var owner: String
public var repository: String public var repository: String
public var state: String
public var title: String public var title: String
public var summary: String public var summary: String
public var meta: String public var meta: String
// Default memberwise initializers are never public by default, so we // Default memberwise initializers are never public by default, so we
// declare one manually. // declare one manually.
public init(number: Int64, owner: String, repository: String, title: String, summary: String, meta: String) { public init(number: Int64, owner: String, repository: String, state: String, title: String, summary: String, meta: String) {
self.number = number self.number = number
self.owner = owner self.owner = owner
self.repository = repository self.repository = repository
self.state = state
self.title = title self.title = title
self.summary = summary self.summary = summary
self.meta = meta self.meta = meta
@@ -2909,6 +2919,7 @@ public struct FfiConverterTypePullRow: FfiConverterRustBuffer {
number: FfiConverterInt64.read(from: &buf), number: FfiConverterInt64.read(from: &buf),
owner: FfiConverterString.read(from: &buf), owner: FfiConverterString.read(from: &buf),
repository: FfiConverterString.read(from: &buf), repository: FfiConverterString.read(from: &buf),
state: FfiConverterString.read(from: &buf),
title: FfiConverterString.read(from: &buf), title: FfiConverterString.read(from: &buf),
summary: FfiConverterString.read(from: &buf), summary: FfiConverterString.read(from: &buf),
meta: FfiConverterString.read(from: &buf) meta: FfiConverterString.read(from: &buf)
@@ -2919,6 +2930,7 @@ public struct FfiConverterTypePullRow: FfiConverterRustBuffer {
FfiConverterInt64.write(value.number, into: &buf) FfiConverterInt64.write(value.number, into: &buf)
FfiConverterString.write(value.owner, into: &buf) FfiConverterString.write(value.owner, into: &buf)
FfiConverterString.write(value.repository, into: &buf) FfiConverterString.write(value.repository, into: &buf)
FfiConverterString.write(value.state, into: &buf)
FfiConverterString.write(value.title, into: &buf) FfiConverterString.write(value.title, into: &buf)
FfiConverterString.write(value.summary, into: &buf) FfiConverterString.write(value.summary, into: &buf)
FfiConverterString.write(value.meta, into: &buf) FfiConverterString.write(value.meta, into: &buf)

View File

@@ -518,6 +518,7 @@ final class MilestoneViewController: RefreshingTableViewController {
super.viewDidLoad() super.viewDidLoad()
tableView.register(MilestoneCell.self, forCellReuseIdentifier: "milestone") tableView.register(MilestoneCell.self, forCellReuseIdentifier: "milestone")
tableView.register(IssueCell.self, forCellReuseIdentifier: "issue") tableView.register(IssueCell.self, forCellReuseIdentifier: "issue")
tableView.register(PullCell.self, forCellReuseIdentifier: "pull")
tableView.rowHeight = UITableView.automaticDimension tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 118 tableView.estimatedRowHeight = 118
let editButton = UIBarButtonItem( let editButton = UIBarButtonItem(
@@ -633,11 +634,8 @@ final class MilestoneViewController: RefreshingTableViewController {
cell.configure(page.issues[indexPath.row]) cell.configure(page.issues[indexPath.row])
return cell return cell
} }
let cell = tableView.dequeueReusableCell(withIdentifier: "pull") let cell = tableView.dequeueReusableCell(withIdentifier: "pull", for: indexPath) as! PullCell
?? UITableViewCell(style: .subtitle, reuseIdentifier: "pull") cell.configure(page.pulls[indexPath.row])
let pull = page.pulls[indexPath.row]
configureTextCell(cell, title: pull.title, detail: "\(pull.summary)\n\(pull.meta)")
cell.accessoryType = .disclosureIndicator
return cell return cell
} }
@@ -668,6 +666,7 @@ final class MilestoneViewController: RefreshingTableViewController {
} }
final class MilestoneCell: UITableViewCell { final class MilestoneCell: UITableViewCell {
private let stateIcon = UIImageView()
private let titleLabel = UILabel() private let titleLabel = UILabel()
private let descriptionLabel = UILabel() private let descriptionLabel = UILabel()
private let metaLabel = UILabel() private let metaLabel = UILabel()
@@ -677,6 +676,9 @@ final class MilestoneCell: UITableViewCell {
super.init(style: style, reuseIdentifier: reuseIdentifier) super.init(style: style, reuseIdentifier: reuseIdentifier)
titleLabel.font = .preferredFont(forTextStyle: .headline) titleLabel.font = .preferredFont(forTextStyle: .headline)
titleLabel.numberOfLines = 2 titleLabel.numberOfLines = 2
let titleStack = UIStackView(arrangedSubviews: [stateIcon, titleLabel])
titleStack.alignment = .firstBaseline
titleStack.spacing = 8
descriptionLabel.font = .preferredFont(forTextStyle: .subheadline) descriptionLabel.font = .preferredFont(forTextStyle: .subheadline)
descriptionLabel.textColor = .secondaryLabel descriptionLabel.textColor = .secondaryLabel
descriptionLabel.numberOfLines = 2 descriptionLabel.numberOfLines = 2
@@ -684,7 +686,9 @@ final class MilestoneCell: UITableViewCell {
metaLabel.textColor = .tertiaryLabel metaLabel.textColor = .tertiaryLabel
metaLabel.numberOfLines = 2 metaLabel.numberOfLines = 2
progress.progressTintColor = .systemGreen progress.progressTintColor = .systemGreen
let stack = UIStackView(arrangedSubviews: [titleLabel, descriptionLabel, progress, metaLabel]) let stack = UIStackView(
arrangedSubviews: [titleStack, descriptionLabel, progress, metaLabel]
)
stack.axis = .vertical stack.axis = .vertical
stack.spacing = 7 stack.spacing = 7
stack.translatesAutoresizingMaskIntoConstraints = false stack.translatesAutoresizingMaskIntoConstraints = false
@@ -702,6 +706,12 @@ final class MilestoneCell: UITableViewCell {
func configure(_ row: MilestoneRow, disclosure: Bool) { func configure(_ row: MilestoneRow, disclosure: Bool) {
accessoryType = disclosure ? .disclosureIndicator : .none accessoryType = disclosure ? .disclosureIndicator : .none
configureOpenClosedStateIcon(
stateIcon,
state: row.state,
subject: "milestone",
textStyle: .headline
)
titleLabel.text = row.title titleLabel.text = row.title
descriptionLabel.text = row.description descriptionLabel.text = row.description
metaLabel.text = row.meta metaLabel.text = row.meta
@@ -712,6 +722,58 @@ final class MilestoneCell: UITableViewCell {
} }
} }
final class PullCell: UITableViewCell {
private let stateIcon = UIImageView()
private let titleLabel = UILabel()
private let summaryLabel = UILabel()
private let metaLabel = UILabel()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
accessoryType = .disclosureIndicator
titleLabel.font = .preferredFont(forTextStyle: .headline)
titleLabel.numberOfLines = 2
let titleStack = UIStackView(arrangedSubviews: [stateIcon, titleLabel])
titleStack.alignment = .firstBaseline
titleStack.spacing = 8
summaryLabel.font = .preferredFont(forTextStyle: .subheadline)
summaryLabel.textColor = .secondaryLabel
summaryLabel.numberOfLines = 2
metaLabel.font = .preferredFont(forTextStyle: .caption1)
metaLabel.textColor = .tertiaryLabel
metaLabel.numberOfLines = 2
[titleLabel, summaryLabel, metaLabel].forEach {
$0.adjustsFontForContentSizeCategory = true
}
let stack = UIStackView(arrangedSubviews: [titleStack, summaryLabel, metaLabel])
stack.axis = .vertical
stack.spacing = 6
stack.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(stack)
NSLayoutConstraint.activate([
stack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
stack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -8),
stack.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10),
stack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -10),
])
}
@available(*, unavailable)
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
func configure(_ row: PullRow) {
configureOpenClosedStateIcon(
stateIcon,
state: row.state,
subject: "pull request",
textStyle: .headline
)
titleLabel.text = row.title
summaryLabel.text = row.summary
metaLabel.text = row.meta
}
}
@MainActor @MainActor
final class PullsViewController: RefreshingTableViewController { final class PullsViewController: RefreshingTableViewController {
private let context: AppContext private let context: AppContext
@@ -733,6 +795,9 @@ final class PullsViewController: RefreshingTableViewController {
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
tableView.register(PullCell.self, forCellReuseIdentifier: "pull")
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 116
navigationItem.leftBarButtonItem = UIBarButtonItem( navigationItem.leftBarButtonItem = UIBarButtonItem(
image: context.symbol("server.rack"), image: context.symbol("server.rack"),
primaryAction: UIAction { [weak self] _ in primaryAction: UIAction { [weak self] _ in
@@ -821,22 +886,11 @@ final class PullsViewController: RefreshingTableViewController {
_ tableView: UITableView, _ tableView: UITableView,
cellForRowAt indexPath: IndexPath cellForRowAt indexPath: IndexPath
) -> UITableViewCell { ) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "pull") let cell = tableView.dequeueReusableCell(withIdentifier: "pull", for: indexPath) as! PullCell
?? UITableViewCell(style: .subtitle, reuseIdentifier: "pull") cell.configure(rows[indexPath.row])
let row = rows[indexPath.row]
configureTextCell(
cell,
title: row.title,
detail: "\(row.summary)\n\(row.meta)"
)
cell.accessoryType = .disclosureIndicator
return cell return cell
} }
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
116
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true) tableView.deselectRow(at: indexPath, animated: true)
let row = rows[indexPath.row] let row = rows[indexPath.row]

View File

@@ -227,7 +227,8 @@ final class IssueViewController: MarkdownPageViewController {
if let page { if let page {
replaceContent(detailViews( replaceContent(detailViews(
title: page.title, title: page.title,
issueState: page.state, state: page.state,
stateSubject: "issue",
meta: page.meta, meta: page.meta,
body: page.body, body: page.body,
comments: page.comments, comments: page.comments,
@@ -305,6 +306,8 @@ final class PullViewController: MarkdownPageViewController {
if let page { if let page {
var views = detailViews( var views = detailViews(
title: page.title, title: page.title,
state: page.state,
stateSubject: "pull request",
meta: page.meta, meta: page.meta,
body: page.body, body: page.body,
comments: [] comments: []
@@ -1138,7 +1141,8 @@ final class DiffViewController: UIViewController {
private func detailViews( private func detailViews(
title: String, title: String,
issueState: String? = nil, state: String? = nil,
stateSubject: String = "",
meta: String, meta: String,
body: String, body: String,
comments: [CommentRow], comments: [CommentRow],
@@ -1156,7 +1160,10 @@ private func detailViews(
metaLabel.textColor = .secondaryLabel metaLabel.textColor = .secondaryLabel
metaLabel.numberOfLines = 0 metaLabel.numberOfLines = 0
let bodyView = markdownView(body) let bodyView = markdownView(body)
var views: [UIView] = [issueState.map { issueTitle(titleLabel, state: $0) } ?? titleLabel, metaLabel] var views: [UIView] = [
state.map { stateTitle(titleLabel, state: $0, subject: stateSubject) } ?? titleLabel,
metaLabel,
]
if !milestone.isEmpty { if !milestone.isEmpty {
let milestoneLabel = UILabel() let milestoneLabel = UILabel()
milestoneLabel.attributedText = symbolText( milestoneLabel.attributedText = symbolText(
@@ -1172,9 +1179,9 @@ private func detailViews(
return views + [separator(), bodyView] + commentViews(comments, editComment: editComment) return views + [separator(), bodyView] + commentViews(comments, editComment: editComment)
} }
private func issueTitle(_ titleLabel: UILabel, state: String) -> UIView { private func stateTitle(_ titleLabel: UILabel, state: String, subject: String) -> UIView {
let icon = UIImageView() let icon = UIImageView()
configureIssueStateIcon(icon, state: state, textStyle: .title1) configureOpenClosedStateIcon(icon, state: state, subject: subject, textStyle: .title1)
let stack = UIStackView(arrangedSubviews: [icon, titleLabel]) let stack = UIStackView(arrangedSubviews: [icon, titleLabel])
stack.axis = .horizontal stack.axis = .horizontal
stack.alignment = .firstBaseline stack.alignment = .firstBaseline

View File

@@ -267,17 +267,32 @@ func configureIssueStateIcon(
_ icon: UIImageView, _ icon: UIImageView,
state: String, state: String,
textStyle: UIFont.TextStyle textStyle: UIFont.TextStyle
) {
configureOpenClosedStateIcon(icon, state: state, subject: "issue", textStyle: textStyle)
}
func configureOpenClosedStateIcon(
_ icon: UIImageView,
state: String,
subject: String,
textStyle: UIFont.TextStyle
) { ) {
let symbol: String let symbol: String
let color: UIColor let color: UIColor
let accessibilityLabel: String let accessibilityLabel: String
switch state { switch state {
case "open": case "open":
(symbol, color, accessibilityLabel) = ("exclamationmark.circle.fill", .systemGreen, "Open issue") (symbol, color, accessibilityLabel) = (
"exclamationmark.circle.fill", .systemGreen, "Open \(subject)"
)
case "closed": case "closed":
(symbol, color, accessibilityLabel) = ("checkmark.circle.fill", .systemPurple, "Closed issue") (symbol, color, accessibilityLabel) = (
"checkmark.circle.fill", .systemPurple, "Closed \(subject)"
)
default: default:
(symbol, color, accessibilityLabel) = ("questionmark.circle.fill", .systemGray, "Unknown issue state") (symbol, color, accessibilityLabel) = (
"questionmark.circle.fill", .systemGray, "Unknown \(subject) state"
)
} }
icon.image = UIImage(systemName: symbol) icon.image = UIImage(systemName: symbol)
icon.tintColor = color icon.tintColor = color