gitbay/Views/Shared/FilterSheet.swift

36835f99e6d463f3e18c5c3fc3562bb92513f52b
gitbay-ios/gitbay/Views/Shared/FilterSheet.swift history · blame · raw

128 lines · 4384 bytes

3 symbols in this file
  1import SwiftUI
  2
  3/// The issue and MR lists' filter sheet. A `Form` with the fields both
  4/// lists share — author and milestone — plus an `extra` slot for the
  5/// rows each list adds: the label picker on both, and an assignee field
  6/// on issues only, since `mr list` takes no `--assignee`.
  7struct FilterSheet<Extra: View>: View {
  8
  9    let title: String
 10    @Binding var author: String?
 11    @Binding var milestone: String?
 12    let milestones: [Milestone]
 13    let onClear: () -> Void
 14    @ViewBuilder var extra: Extra
 15
 16    @Environment(\.dismiss) private var dismiss
 17
 18    var body: some View {
 19        NavigationStack {
 20            Form {
 21                extra
 22                Section("Author") {
 23                    TextField("Username", text: $author.orEmpty)
 24                        .autocorrectionDisabled()
 25                        .textInputAutocapitalization(.never)
 26                        .accessibilityIdentifier("filter-author")
 27                }
 28                Section("Milestone") {
 29                    Menu {
 30                        Button("Any milestone") { milestone = nil }
 31                        Button("No milestone") { milestone = "none" }
 32                        ForEach(milestones) { entry in
 33                            Button(entry.title) { milestone = entry.title }
 34                        }
 35                    } label: {
 36                        HStack {
 37                            Text(milestone ?? "Any milestone")
 38                            Spacer()
 39                            Image(systemName: "chevron.up.chevron.down")
 40                                .font(.gbSans(.caption2))
 41                                .foregroundStyle(.secondary)
 42                        }
 43                    }
 44                    .accessibilityIdentifier("filter-milestone-menu")
 45                }
 46                Section {
 47                    Button("Clear filters", role: .destructive, action: onClear)
 48                }
 49            }
 50            .navigationTitle(title)
 51            .navigationBarTitleDisplayMode(.inline)
 52            .toolbar {
 53                ToolbarItem(placement: .confirmationAction) {
 54                    Button("Done") { dismiss() }
 55                }
 56            }
 57        }
 58    }
 59}
 60
 61/// The label picker both lists' sheets add through `FilterSheet`'s
 62/// `extra` slot.
 63struct LabelFilterSection: View {
 64
 65    @Binding var label: String?
 66    let labels: [IssueLabel]
 67    let colors: LabelColors
 68
 69    var body: some View {
 70        Section("Label") {
 71            Menu {
 72                Button("Any label") { label = nil }
 73                ForEach(labels) { entry in
 74                    Button {
 75                        label = entry.name
 76                    } label: {
 77                        Text(entry.name)
 78                    }
 79                }
 80            } label: {
 81                HStack {
 82                    if let label {
 83                        GBChip(label, colors.color(label))
 84                    } else {
 85                        Text("Any label")
 86                    }
 87                    Spacer()
 88                    Image(systemName: "chevron.up.chevron.down")
 89                        .font(.gbSans(.caption2))
 90                        .foregroundStyle(.secondary)
 91                }
 92            }
 93            .accessibilityIdentifier("filter-label-menu")
 94        }
 95    }
 96}
 97
 98/// The label and assignee rows the issue list's sheet adds through
 99/// `FilterSheet`'s `extra` slot.
100struct IssueFilterExtra: View {
101
102    @Binding var label: String?
103    @Binding var assignee: String?
104    let labels: [IssueLabel]
105    let colors: LabelColors
106
107    var body: some View {
108        LabelFilterSection(label: $label, labels: labels, colors: colors)
109        Section("Assignee") {
110            TextField("Username", text: $assignee.orEmpty)
111                .autocorrectionDisabled()
112                .textInputAutocapitalization(.never)
113                .accessibilityIdentifier("filter-assignee")
114        }
115    }
116}
117
118extension Binding where Value == String? {
119    /// Bridges an optional filter field to a plain `TextField`: empty
120    /// text writes back `nil` rather than `""`, since the two are
121    /// distinct filter values (no flag sent versus `--flag ""`).
122    var orEmpty: Binding<String> {
123        Binding<String>(
124            get: { self.wrappedValue ?? "" },
125            set: { self.wrappedValue = $0.isEmpty ? nil : $0 }
126        )
127    }
128}