Show labels on issue details
This commit is contained in:
@@ -183,8 +183,10 @@ xcrun simctl launch booted de.rfc1437.gotcha
|
||||
issue list/detail remains usable with Dynamic Type and VoiceOver labels.
|
||||
- [ ] Open both an open and a closed issue. A green open or purple closed icon
|
||||
appears beside the title and VoiceOver announces the state; author
|
||||
metadata, Markdown body, and comments remain correct. Links and
|
||||
selectable text use normal iOS interaction.
|
||||
metadata, every colored label, Markdown body, and comments remain correct.
|
||||
Multiple labels wrap without clipping at large Dynamic Type sizes and
|
||||
VoiceOver announces each label. Links and selectable text use normal iOS
|
||||
interaction.
|
||||
- [ ] Tap the **Add comment** icon on an issue. Enter headings, emphasis, a
|
||||
list, link, and fenced code block; **Write** syntax-highlights the
|
||||
Markdown, **Preview** renders it, and switching modes preserves the exact
|
||||
|
||||
@@ -77,6 +77,7 @@ pub struct IssuePage {
|
||||
pub state: String,
|
||||
pub meta: String,
|
||||
pub milestone: String,
|
||||
pub labels: Vec<LabelRow>,
|
||||
pub body: String,
|
||||
pub comments: Vec<CommentRow>,
|
||||
pub has_more: bool,
|
||||
@@ -554,6 +555,14 @@ pub fn issue_page(details: IssueDetails) -> IssuePage {
|
||||
.unwrap_or_else(|| "unknown".into()),
|
||||
meta: issue_meta(&details.issue),
|
||||
milestone: issue_milestone(&details.issue),
|
||||
labels: details
|
||||
.issue
|
||||
.labels
|
||||
.as_deref()
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.filter_map(label_row)
|
||||
.collect(),
|
||||
body: details
|
||||
.issue
|
||||
.body
|
||||
@@ -1269,6 +1278,11 @@ mod tests {
|
||||
let page = issue_page(IssueDetails {
|
||||
issue: models::Issue {
|
||||
state: Some("closed".into()),
|
||||
labels: Some(vec![models::Label {
|
||||
name: Some("bug".into()),
|
||||
color: Some("ff0000".into()),
|
||||
..Default::default()
|
||||
}]),
|
||||
..Default::default()
|
||||
},
|
||||
comments: vec![
|
||||
@@ -1297,6 +1311,7 @@ mod tests {
|
||||
});
|
||||
|
||||
assert_eq!(page.state, "closed");
|
||||
assert_eq!(page.labels[0].name, "bug");
|
||||
assert_eq!(page.comments[0].id, 7);
|
||||
assert!(page.comments[0].can_edit);
|
||||
assert!(!page.comments[1].can_edit);
|
||||
|
||||
@@ -2236,17 +2236,19 @@ public struct IssuePage: Equatable, Hashable {
|
||||
public var state: String
|
||||
public var meta: String
|
||||
public var milestone: String
|
||||
public var labels: [LabelRow]
|
||||
public var body: String
|
||||
public var comments: [CommentRow]
|
||||
public var hasMore: Bool
|
||||
|
||||
// Default memberwise initializers are never public by default, so we
|
||||
// declare one manually.
|
||||
public init(title: String, state: String, meta: String, milestone: String, body: String, comments: [CommentRow], hasMore: Bool) {
|
||||
public init(title: String, state: String, meta: String, milestone: String, labels: [LabelRow], body: String, comments: [CommentRow], hasMore: Bool) {
|
||||
self.title = title
|
||||
self.state = state
|
||||
self.meta = meta
|
||||
self.milestone = milestone
|
||||
self.labels = labels
|
||||
self.body = body
|
||||
self.comments = comments
|
||||
self.hasMore = hasMore
|
||||
@@ -2272,6 +2274,7 @@ public struct FfiConverterTypeIssuePage: FfiConverterRustBuffer {
|
||||
state: FfiConverterString.read(from: &buf),
|
||||
meta: FfiConverterString.read(from: &buf),
|
||||
milestone: FfiConverterString.read(from: &buf),
|
||||
labels: FfiConverterSequenceTypeLabelRow.read(from: &buf),
|
||||
body: FfiConverterString.read(from: &buf),
|
||||
comments: FfiConverterSequenceTypeCommentRow.read(from: &buf),
|
||||
hasMore: FfiConverterBool.read(from: &buf)
|
||||
@@ -2283,6 +2286,7 @@ public struct FfiConverterTypeIssuePage: FfiConverterRustBuffer {
|
||||
FfiConverterString.write(value.state, into: &buf)
|
||||
FfiConverterString.write(value.meta, into: &buf)
|
||||
FfiConverterString.write(value.milestone, into: &buf)
|
||||
FfiConverterSequenceTypeLabelRow.write(value.labels, into: &buf)
|
||||
FfiConverterString.write(value.body, into: &buf)
|
||||
FfiConverterSequenceTypeCommentRow.write(value.comments, into: &buf)
|
||||
FfiConverterBool.write(value.hasMore, into: &buf)
|
||||
|
||||
@@ -370,14 +370,7 @@ final class IssueCell: UITableViewCell {
|
||||
labels.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||||
labels.isHidden = row.labels.isEmpty
|
||||
for label in row.labels.prefix(3) {
|
||||
let view = UILabel()
|
||||
view.text = " \(label.name) "
|
||||
view.font = .preferredFont(forTextStyle: .caption2)
|
||||
view.textColor = label.light ? .black : .white
|
||||
view.backgroundColor = UIColor(hex: label.color)
|
||||
view.layer.cornerRadius = 9
|
||||
view.clipsToBounds = true
|
||||
labels.addArrangedSubview(view)
|
||||
labels.addArrangedSubview(issueLabelView(label))
|
||||
}
|
||||
labels.addArrangedSubview(UIView())
|
||||
}
|
||||
|
||||
@@ -232,7 +232,8 @@ final class IssueViewController: MarkdownPageViewController {
|
||||
body: page.body,
|
||||
comments: page.comments,
|
||||
editComment: { [weak self] comment in self?.editComment(comment) },
|
||||
milestone: page.milestone
|
||||
milestone: page.milestone,
|
||||
labels: page.labels
|
||||
))
|
||||
finishPagination(hasMore: page.hasMore)
|
||||
}
|
||||
@@ -1142,7 +1143,8 @@ private func detailViews(
|
||||
body: String,
|
||||
comments: [CommentRow],
|
||||
editComment: ((CommentRow) -> Void)? = nil,
|
||||
milestone: String = ""
|
||||
milestone: String = "",
|
||||
labels: [LabelRow] = []
|
||||
) -> [UIView] {
|
||||
let titleLabel = UILabel()
|
||||
titleLabel.text = title
|
||||
@@ -1166,6 +1168,7 @@ private func detailViews(
|
||||
milestoneLabel.accessibilityLabel = "Milestone \(milestone)"
|
||||
views.append(milestoneLabel)
|
||||
}
|
||||
if !labels.isEmpty { views.append(IssueLabelsView(labels)) }
|
||||
return views + [separator(), bodyView] + commentViews(comments, editComment: editComment)
|
||||
}
|
||||
|
||||
|
||||
@@ -287,3 +287,74 @@ func configureIssueStateIcon(
|
||||
icon.isAccessibilityElement = true
|
||||
icon.accessibilityLabel = accessibilityLabel
|
||||
}
|
||||
|
||||
func issueLabelView(_ label: LabelRow) -> UILabel {
|
||||
let view = UILabel()
|
||||
view.text = " \(label.name) "
|
||||
view.font = .preferredFont(forTextStyle: .caption2)
|
||||
view.adjustsFontForContentSizeCategory = true
|
||||
view.textColor = label.light ? .black : .white
|
||||
view.backgroundColor = UIColor(hex: label.color)
|
||||
view.layer.cornerRadius = 9
|
||||
view.clipsToBounds = true
|
||||
view.accessibilityLabel = "Label \(label.name)"
|
||||
return view
|
||||
}
|
||||
|
||||
final class IssueLabelsView: UIView {
|
||||
private let labels: [UILabel]
|
||||
private var contentHeight: CGFloat
|
||||
private let spacing: CGFloat = 6
|
||||
|
||||
init(_ rows: [LabelRow]) {
|
||||
labels = rows.map(issueLabelView)
|
||||
contentHeight = labels.first?.intrinsicContentSize.height ?? 0
|
||||
super.init(frame: .zero)
|
||||
labels.forEach(addSubview)
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(contentSizeCategoryDidChange),
|
||||
name: UIContentSizeCategory.didChangeNotification,
|
||||
object: nil
|
||||
)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
|
||||
|
||||
override var intrinsicContentSize: CGSize {
|
||||
CGSize(width: UIView.noIntrinsicMetric, height: contentHeight)
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
guard bounds.width > 0 else { return }
|
||||
var origin = CGPoint.zero
|
||||
var rowHeight: CGFloat = 0
|
||||
for label in labels {
|
||||
let size = label.sizeThatFits(
|
||||
CGSize(width: bounds.width, height: .greatestFiniteMagnitude)
|
||||
)
|
||||
if origin.x > 0, origin.x + size.width > bounds.width {
|
||||
origin.x = 0
|
||||
origin.y += rowHeight + spacing
|
||||
rowHeight = 0
|
||||
}
|
||||
label.frame = CGRect(origin: origin, size: size)
|
||||
origin.x += size.width + spacing
|
||||
rowHeight = max(rowHeight, size.height)
|
||||
}
|
||||
let height = origin.y + rowHeight
|
||||
if contentHeight != height {
|
||||
contentHeight = height
|
||||
invalidateIntrinsicContentSize()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func contentSizeCategoryDidChange() {
|
||||
labels.forEach { $0.font = .preferredFont(forTextStyle: .caption2) }
|
||||
contentHeight = labels.first?.intrinsicContentSize.height ?? 0
|
||||
invalidateIntrinsicContentSize()
|
||||
setNeedsLayout()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user