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.
|
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
|
- [ ] 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
|
appears beside the title and VoiceOver announces the state; author
|
||||||
metadata, Markdown body, and comments remain correct. Links and
|
metadata, every colored label, Markdown body, and comments remain correct.
|
||||||
selectable text use normal iOS interaction.
|
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
|
- [ ] Tap the **Add comment** icon on an issue. Enter headings, emphasis, a
|
||||||
list, link, and fenced code block; **Write** syntax-highlights the
|
list, link, and fenced code block; **Write** syntax-highlights the
|
||||||
Markdown, **Preview** renders it, and switching modes preserves the exact
|
Markdown, **Preview** renders it, and switching modes preserves the exact
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ pub struct IssuePage {
|
|||||||
pub state: String,
|
pub state: String,
|
||||||
pub meta: String,
|
pub meta: String,
|
||||||
pub milestone: String,
|
pub milestone: String,
|
||||||
|
pub labels: Vec<LabelRow>,
|
||||||
pub body: String,
|
pub body: String,
|
||||||
pub comments: Vec<CommentRow>,
|
pub comments: Vec<CommentRow>,
|
||||||
pub has_more: bool,
|
pub has_more: bool,
|
||||||
@@ -554,6 +555,14 @@ pub fn issue_page(details: IssueDetails) -> IssuePage {
|
|||||||
.unwrap_or_else(|| "unknown".into()),
|
.unwrap_or_else(|| "unknown".into()),
|
||||||
meta: issue_meta(&details.issue),
|
meta: issue_meta(&details.issue),
|
||||||
milestone: issue_milestone(&details.issue),
|
milestone: issue_milestone(&details.issue),
|
||||||
|
labels: details
|
||||||
|
.issue
|
||||||
|
.labels
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.iter()
|
||||||
|
.filter_map(label_row)
|
||||||
|
.collect(),
|
||||||
body: details
|
body: details
|
||||||
.issue
|
.issue
|
||||||
.body
|
.body
|
||||||
@@ -1269,6 +1278,11 @@ mod tests {
|
|||||||
let page = issue_page(IssueDetails {
|
let page = issue_page(IssueDetails {
|
||||||
issue: models::Issue {
|
issue: models::Issue {
|
||||||
state: Some("closed".into()),
|
state: Some("closed".into()),
|
||||||
|
labels: Some(vec![models::Label {
|
||||||
|
name: Some("bug".into()),
|
||||||
|
color: Some("ff0000".into()),
|
||||||
|
..Default::default()
|
||||||
|
}]),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
comments: vec![
|
comments: vec![
|
||||||
@@ -1297,6 +1311,7 @@ mod tests {
|
|||||||
});
|
});
|
||||||
|
|
||||||
assert_eq!(page.state, "closed");
|
assert_eq!(page.state, "closed");
|
||||||
|
assert_eq!(page.labels[0].name, "bug");
|
||||||
assert_eq!(page.comments[0].id, 7);
|
assert_eq!(page.comments[0].id, 7);
|
||||||
assert!(page.comments[0].can_edit);
|
assert!(page.comments[0].can_edit);
|
||||||
assert!(!page.comments[1].can_edit);
|
assert!(!page.comments[1].can_edit);
|
||||||
|
|||||||
@@ -2236,17 +2236,19 @@ public struct IssuePage: Equatable, Hashable {
|
|||||||
public var state: String
|
public var state: String
|
||||||
public var meta: String
|
public var meta: String
|
||||||
public var milestone: String
|
public var milestone: String
|
||||||
|
public var labels: [LabelRow]
|
||||||
public var body: String
|
public var body: String
|
||||||
public var comments: [CommentRow]
|
public var comments: [CommentRow]
|
||||||
public var hasMore: Bool
|
public var hasMore: Bool
|
||||||
|
|
||||||
// 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, 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.title = title
|
||||||
self.state = state
|
self.state = state
|
||||||
self.meta = meta
|
self.meta = meta
|
||||||
self.milestone = milestone
|
self.milestone = milestone
|
||||||
|
self.labels = labels
|
||||||
self.body = body
|
self.body = body
|
||||||
self.comments = comments
|
self.comments = comments
|
||||||
self.hasMore = hasMore
|
self.hasMore = hasMore
|
||||||
@@ -2272,6 +2274,7 @@ public struct FfiConverterTypeIssuePage: FfiConverterRustBuffer {
|
|||||||
state: FfiConverterString.read(from: &buf),
|
state: FfiConverterString.read(from: &buf),
|
||||||
meta: FfiConverterString.read(from: &buf),
|
meta: FfiConverterString.read(from: &buf),
|
||||||
milestone: FfiConverterString.read(from: &buf),
|
milestone: FfiConverterString.read(from: &buf),
|
||||||
|
labels: FfiConverterSequenceTypeLabelRow.read(from: &buf),
|
||||||
body: FfiConverterString.read(from: &buf),
|
body: FfiConverterString.read(from: &buf),
|
||||||
comments: FfiConverterSequenceTypeCommentRow.read(from: &buf),
|
comments: FfiConverterSequenceTypeCommentRow.read(from: &buf),
|
||||||
hasMore: FfiConverterBool.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.state, into: &buf)
|
||||||
FfiConverterString.write(value.meta, into: &buf)
|
FfiConverterString.write(value.meta, into: &buf)
|
||||||
FfiConverterString.write(value.milestone, into: &buf)
|
FfiConverterString.write(value.milestone, into: &buf)
|
||||||
|
FfiConverterSequenceTypeLabelRow.write(value.labels, into: &buf)
|
||||||
FfiConverterString.write(value.body, into: &buf)
|
FfiConverterString.write(value.body, into: &buf)
|
||||||
FfiConverterSequenceTypeCommentRow.write(value.comments, into: &buf)
|
FfiConverterSequenceTypeCommentRow.write(value.comments, into: &buf)
|
||||||
FfiConverterBool.write(value.hasMore, into: &buf)
|
FfiConverterBool.write(value.hasMore, into: &buf)
|
||||||
|
|||||||
@@ -370,14 +370,7 @@ final class IssueCell: UITableViewCell {
|
|||||||
labels.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
labels.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||||||
labels.isHidden = row.labels.isEmpty
|
labels.isHidden = row.labels.isEmpty
|
||||||
for label in row.labels.prefix(3) {
|
for label in row.labels.prefix(3) {
|
||||||
let view = UILabel()
|
labels.addArrangedSubview(issueLabelView(label))
|
||||||
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(UIView())
|
labels.addArrangedSubview(UIView())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -232,7 +232,8 @@ final class IssueViewController: MarkdownPageViewController {
|
|||||||
body: page.body,
|
body: page.body,
|
||||||
comments: page.comments,
|
comments: page.comments,
|
||||||
editComment: { [weak self] comment in self?.editComment(comment) },
|
editComment: { [weak self] comment in self?.editComment(comment) },
|
||||||
milestone: page.milestone
|
milestone: page.milestone,
|
||||||
|
labels: page.labels
|
||||||
))
|
))
|
||||||
finishPagination(hasMore: page.hasMore)
|
finishPagination(hasMore: page.hasMore)
|
||||||
}
|
}
|
||||||
@@ -1142,7 +1143,8 @@ private func detailViews(
|
|||||||
body: String,
|
body: String,
|
||||||
comments: [CommentRow],
|
comments: [CommentRow],
|
||||||
editComment: ((CommentRow) -> Void)? = nil,
|
editComment: ((CommentRow) -> Void)? = nil,
|
||||||
milestone: String = ""
|
milestone: String = "",
|
||||||
|
labels: [LabelRow] = []
|
||||||
) -> [UIView] {
|
) -> [UIView] {
|
||||||
let titleLabel = UILabel()
|
let titleLabel = UILabel()
|
||||||
titleLabel.text = title
|
titleLabel.text = title
|
||||||
@@ -1166,6 +1168,7 @@ private func detailViews(
|
|||||||
milestoneLabel.accessibilityLabel = "Milestone \(milestone)"
|
milestoneLabel.accessibilityLabel = "Milestone \(milestone)"
|
||||||
views.append(milestoneLabel)
|
views.append(milestoneLabel)
|
||||||
}
|
}
|
||||||
|
if !labels.isEmpty { views.append(IssueLabelsView(labels)) }
|
||||||
return views + [separator(), bodyView] + commentViews(comments, editComment: editComment)
|
return views + [separator(), bodyView] + commentViews(comments, editComment: editComment)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -287,3 +287,74 @@ func configureIssueStateIcon(
|
|||||||
icon.isAccessibilityElement = true
|
icon.isAccessibilityElement = true
|
||||||
icon.accessibilityLabel = accessibilityLabel
|
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