Show milestone and pull request states
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user