Show issue state icons in lists
This commit is contained in:
@@ -145,6 +145,9 @@ xcrun simctl launch booted de.rfc1437.gotcha
|
|||||||
date, and current favorite state.
|
date, and current favorite state.
|
||||||
- [ ] Toggle a favorite and confirm it remains after refresh and relaunch.
|
- [ ] Toggle a favorite and confirm it remains after refresh and relaunch.
|
||||||
- [ ] Open a repository; the issue list defaults to the saved Open/Closed filter.
|
- [ ] Open a repository; the issue list defaults to the saved Open/Closed filter.
|
||||||
|
- [ ] Repository issue rows and issue rows inside a milestone show a green open
|
||||||
|
or purple closed icon beside the title; mixed milestone results use the
|
||||||
|
correct icon per row and VoiceOver announces each state.
|
||||||
- [ ] Change the native filter menu between Open and Closed; the checkmark,
|
- [ ] Change the native filter menu between Open and Closed; the checkmark,
|
||||||
rows, and persisted selection update.
|
rows, and persisted selection update.
|
||||||
- [ ] Select one and then multiple labels in the issue filter. Checkmarks and
|
- [ ] Select one and then multiple labels in the issue filter. Checkmarks and
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ pub struct LabelRow {
|
|||||||
#[derive(Clone, uniffi::Record)]
|
#[derive(Clone, uniffi::Record)]
|
||||||
pub struct IssueRow {
|
pub struct IssueRow {
|
||||||
pub number: i64,
|
pub number: i64,
|
||||||
|
pub state: String,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub summary: String,
|
pub summary: String,
|
||||||
pub meta: String,
|
pub meta: String,
|
||||||
@@ -288,6 +289,7 @@ pub fn issue_rows(issues: &[models::Issue]) -> Vec<IssueRow> {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|issue| IssueRow {
|
.map(|issue| IssueRow {
|
||||||
number: issue.number.unwrap_or_default(),
|
number: issue.number.unwrap_or_default(),
|
||||||
|
state: issue.state.clone().unwrap_or_else(|| "unknown".into()),
|
||||||
title: issue
|
title: issue
|
||||||
.title
|
.title
|
||||||
.clone()
|
.clone()
|
||||||
@@ -1251,10 +1253,13 @@ mod tests {
|
|||||||
assert_eq!(milestone_row.progress, 0.4);
|
assert_eq!(milestone_row.progress, 0.4);
|
||||||
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()),
|
||||||
milestone: Some(Box::new(milestone)),
|
milestone: Some(Box::new(milestone)),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
assert_eq!(issue_rows(&[issue])[0].milestone, "Version 1");
|
let row = &issue_rows(&[issue])[0];
|
||||||
|
assert_eq!(row.state, "closed");
|
||||||
|
assert_eq!(row.milestone, "Version 1");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -2288,6 +2288,7 @@ public func FfiConverterTypeIssuePage_lower(_ value: IssuePage) -> RustBuffer {
|
|||||||
|
|
||||||
public struct IssueRow: Equatable, Hashable {
|
public struct IssueRow: Equatable, Hashable {
|
||||||
public var number: Int64
|
public var number: Int64
|
||||||
|
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
|
||||||
@@ -2296,8 +2297,9 @@ public struct IssueRow: 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(number: Int64, title: String, summary: String, meta: String, milestone: String, labels: [LabelRow]) {
|
public init(number: Int64, state: String, title: String, summary: String, meta: String, milestone: String, labels: [LabelRow]) {
|
||||||
self.number = number
|
self.number = number
|
||||||
|
self.state = state
|
||||||
self.title = title
|
self.title = title
|
||||||
self.summary = summary
|
self.summary = summary
|
||||||
self.meta = meta
|
self.meta = meta
|
||||||
@@ -2322,6 +2324,7 @@ public struct FfiConverterTypeIssueRow: FfiConverterRustBuffer {
|
|||||||
return
|
return
|
||||||
try IssueRow(
|
try IssueRow(
|
||||||
number: FfiConverterInt64.read(from: &buf),
|
number: FfiConverterInt64.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),
|
||||||
@@ -2332,6 +2335,7 @@ public struct FfiConverterTypeIssueRow: FfiConverterRustBuffer {
|
|||||||
|
|
||||||
public static func write(_ value: IssueRow, into buf: inout [UInt8]) {
|
public static func write(_ value: IssueRow, into buf: inout [UInt8]) {
|
||||||
FfiConverterInt64.write(value.number, into: &buf)
|
FfiConverterInt64.write(value.number, 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)
|
||||||
@@ -4061,4 +4065,4 @@ public func uniffiEnsureGotchaCoreInitialized() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// swiftlint:enable all
|
// swiftlint:enable all
|
||||||
|
|||||||
@@ -282,6 +282,7 @@ final class IssuesViewController: RefreshingTableViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final class IssueCell: UITableViewCell {
|
final class IssueCell: UITableViewCell {
|
||||||
|
private let stateIcon = UIImageView()
|
||||||
private let titleLabel = UILabel()
|
private let titleLabel = UILabel()
|
||||||
private let summaryLabel = UILabel()
|
private let summaryLabel = UILabel()
|
||||||
private let labels = UIStackView()
|
private let labels = UIStackView()
|
||||||
@@ -293,6 +294,9 @@ final class IssueCell: UITableViewCell {
|
|||||||
accessoryType = .disclosureIndicator
|
accessoryType = .disclosureIndicator
|
||||||
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
|
||||||
summaryLabel.font = .preferredFont(forTextStyle: .subheadline)
|
summaryLabel.font = .preferredFont(forTextStyle: .subheadline)
|
||||||
summaryLabel.textColor = .secondaryLabel
|
summaryLabel.textColor = .secondaryLabel
|
||||||
summaryLabel.numberOfLines = 2
|
summaryLabel.numberOfLines = 2
|
||||||
@@ -304,7 +308,7 @@ final class IssueCell: UITableViewCell {
|
|||||||
milestoneLabel.font = .preferredFont(forTextStyle: .caption1)
|
milestoneLabel.font = .preferredFont(forTextStyle: .caption1)
|
||||||
milestoneLabel.adjustsFontForContentSizeCategory = true
|
milestoneLabel.adjustsFontForContentSizeCategory = true
|
||||||
let stack = UIStackView(
|
let stack = UIStackView(
|
||||||
arrangedSubviews: [titleLabel, summaryLabel, labels, metaLabel, milestoneLabel]
|
arrangedSubviews: [titleStack, summaryLabel, labels, metaLabel, milestoneLabel]
|
||||||
)
|
)
|
||||||
stack.axis = .vertical
|
stack.axis = .vertical
|
||||||
stack.spacing = 6
|
stack.spacing = 6
|
||||||
@@ -322,6 +326,7 @@ final class IssueCell: UITableViewCell {
|
|||||||
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
|
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
|
||||||
|
|
||||||
func configure(_ row: IssueRow) {
|
func configure(_ row: IssueRow) {
|
||||||
|
configureIssueStateIcon(stateIcon, state: row.state, textStyle: .headline)
|
||||||
titleLabel.text = row.title
|
titleLabel.text = row.title
|
||||||
summaryLabel.text = row.summary
|
summaryLabel.text = row.summary
|
||||||
metaLabel.text = row.meta
|
metaLabel.text = row.meta
|
||||||
|
|||||||
@@ -1130,23 +1130,8 @@ private func detailViews(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func issueTitle(_ titleLabel: UILabel, state: String) -> UIView {
|
private func issueTitle(_ titleLabel: UILabel, state: String) -> UIView {
|
||||||
let symbol: String
|
let icon = UIImageView()
|
||||||
let color: UIColor
|
configureIssueStateIcon(icon, state: state, textStyle: .title1)
|
||||||
let accessibilityLabel: String
|
|
||||||
switch state {
|
|
||||||
case "open":
|
|
||||||
(symbol, color, accessibilityLabel) = ("exclamationmark.circle.fill", .systemGreen, "Open issue")
|
|
||||||
case "closed":
|
|
||||||
(symbol, color, accessibilityLabel) = ("checkmark.circle.fill", .systemPurple, "Closed issue")
|
|
||||||
default:
|
|
||||||
(symbol, color, accessibilityLabel) = ("questionmark.circle.fill", .systemGray, "Unknown issue state")
|
|
||||||
}
|
|
||||||
let icon = UIImageView(image: UIImage(systemName: symbol))
|
|
||||||
icon.tintColor = color
|
|
||||||
icon.preferredSymbolConfiguration = UIImage.SymbolConfiguration(textStyle: .title1)
|
|
||||||
icon.setContentHuggingPriority(.required, for: .horizontal)
|
|
||||||
icon.isAccessibilityElement = true
|
|
||||||
icon.accessibilityLabel = accessibilityLabel
|
|
||||||
let stack = UIStackView(arrangedSubviews: [icon, titleLabel])
|
let stack = UIStackView(arrangedSubviews: [icon, titleLabel])
|
||||||
stack.axis = .horizontal
|
stack.axis = .horizontal
|
||||||
stack.alignment = .firstBaseline
|
stack.alignment = .firstBaseline
|
||||||
|
|||||||
@@ -255,3 +255,28 @@ func symbolText(_ symbol: String, text: String, font: UIFont, color: UIColor) ->
|
|||||||
]))
|
]))
|
||||||
return output
|
return output
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func configureIssueStateIcon(
|
||||||
|
_ icon: UIImageView,
|
||||||
|
state: String,
|
||||||
|
textStyle: UIFont.TextStyle
|
||||||
|
) {
|
||||||
|
let symbol: String
|
||||||
|
let color: UIColor
|
||||||
|
let accessibilityLabel: String
|
||||||
|
switch state {
|
||||||
|
case "open":
|
||||||
|
(symbol, color, accessibilityLabel) = ("exclamationmark.circle.fill", .systemGreen, "Open issue")
|
||||||
|
case "closed":
|
||||||
|
(symbol, color, accessibilityLabel) = ("checkmark.circle.fill", .systemPurple, "Closed issue")
|
||||||
|
default:
|
||||||
|
(symbol, color, accessibilityLabel) = ("questionmark.circle.fill", .systemGray, "Unknown issue state")
|
||||||
|
}
|
||||||
|
icon.image = UIImage(systemName: symbol)
|
||||||
|
icon.tintColor = color
|
||||||
|
icon.preferredSymbolConfiguration = UIImage.SymbolConfiguration(textStyle: textStyle)
|
||||||
|
icon.setContentHuggingPriority(.required, for: .horizontal)
|
||||||
|
icon.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||||
|
icon.isAccessibilityElement = true
|
||||||
|
icon.accessibilityLabel = accessibilityLabel
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user