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 only issues have, since `mr list` takes neither `--label` nor /// `--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 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 { 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") } 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 } ) } }