import SwiftUI /// The issue and MR lists' filter sheet. A `Form` with the fields both /// lists share — author and milestone — plus an `extra` slot for the /// rows each list adds: the label picker on both, and an assignee field /// on issues only, since `mr list` takes no `--assignee`. struct FilterSheet: View { let title: String @Binding var author: String? @Binding var milestone: String? let milestones: [Milestone] let onClear: () -> Void @ViewBuilder var extra: Extra @Environment(\.dismiss) private var dismiss var body: some View { NavigationStack { Form { extra Section("Author") { TextField("Username", text: $author.orEmpty) .autocorrectionDisabled() .textInputAutocapitalization(.never) .accessibilityIdentifier("filter-author") } Section("Milestone") { Menu { Button("Any milestone") { milestone = nil } Button("No milestone") { milestone = "none" } ForEach(milestones) { entry in Button(entry.title) { milestone = entry.title } } } label: { HStack { Text(milestone ?? "Any milestone") Spacer() Image(systemName: "chevron.up.chevron.down") .font(.gbSans(.caption2)) .foregroundStyle(.secondary) } } .accessibilityIdentifier("filter-milestone-menu") } Section { Button("Clear filters", role: .destructive, action: onClear) } } .navigationTitle(title) .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .confirmationAction) { Button("Done") { dismiss() } } } } } } /// The label picker both lists' sheets add through `FilterSheet`'s /// `extra` slot. struct LabelFilterSection: View { @Binding var label: String? let labels: [IssueLabel] let colors: LabelColors var body: some View { Section("Label") { Menu { Button("Any label") { label = nil } ForEach(labels) { entry in Button { label = entry.name } label: { Text(entry.name) } } } label: { HStack { if let label { GBChip(label, colors.color(label)) } else { Text("Any label") } Spacer() Image(systemName: "chevron.up.chevron.down") .font(.gbSans(.caption2)) .foregroundStyle(.secondary) } } .accessibilityIdentifier("filter-label-menu") } } } /// The label and assignee rows the issue list's sheet adds through /// `FilterSheet`'s `extra` slot. struct IssueFilterExtra: View { @Binding var label: String? @Binding var assignee: String? let labels: [IssueLabel] let colors: LabelColors var body: some View { LabelFilterSection(label: $label, labels: labels, colors: colors) Section("Assignee") { TextField("Username", text: $assignee.orEmpty) .autocorrectionDisabled() .textInputAutocapitalization(.never) .accessibilityIdentifier("filter-assignee") } } } extension Binding where Value == String? { /// Bridges an optional filter field to a plain `TextField`: empty /// text writes back `nil` rather than `""`, since the two are /// distinct filter values (no flag sent versus `--flag ""`). var orEmpty: Binding { Binding( get: { self.wrappedValue ?? "" }, set: { self.wrappedValue = $0.isEmpty ? nil : $0 } ) } }