gitbay/Views/Issues/LabelListView.swift

v1.1.1
gitbay-ios/gitbay/Views/Issues/LabelListView.swift history · blame · raw

108 lines · 3956 bytes

  1import SwiftUI
  2
  3/// A repository's labels: their colour, how many issues carry each, and
  4/// the controls to create, recolour and remove them.
  5struct LabelListView: View {
  6
  7    @State private var model: LabelListViewModel
  8    @State private var newLabel = ""
  9    @State private var pendingRemoval: IssueLabel?
 10
 11    init(client: GitbayClient, repo: String) {
 12        _model = State(initialValue: LabelListViewModel(client: client, repoPath: repo))
 13    }
 14
 15    private var colors: LabelColors { LabelColors(model.state.value ?? []) }
 16
 17    var body: some View {
 18        List {
 19            if let actionError = model.actionError {
 20                Section { GBNotice(actionError) }
 21            }
 22
 23            Section("New label") {
 24                HStack {
 25                    TextField("Label", text: $newLabel)
 26                        .autocorrectionDisabled()
 27                        .textInputAutocapitalization(.never)
 28                    Button {
 29                        let name = newLabel.trimmingCharacters(in: .whitespaces)
 30                        newLabel = ""
 31                        Task { await model.setLabel(name, color: nil) }
 32                    } label: {
 33                        Image(systemName: "plus.circle.fill")
 34                    }
 35                    .disabled(
 36                        newLabel.trimmingCharacters(in: .whitespaces).isEmpty || model.working
 37                    )
 38                }
 39            }
 40
 41            ForEach(model.state.value ?? []) { label in
 42                LabelRow(label: label, color: colors.color(label.name), working: model.working) {
 43                    hex in
 44                    Task { await model.setLabel(label.name, color: hex) }
 45                } remove: {
 46                    pendingRemoval = label
 47                }
 48            }
 49        }
 50        .overlay { LoadStateOverlay(state: model.state) }
 51        .navigationTitle("Labels")
 52        .navigationBarTitleDisplayMode(.inline)
 53        .task { await model.load() }
 54        .refreshable { await model.load() }
 55        .confirmationDialog(
 56            "Remove \(pendingRemoval?.name ?? "")?",
 57            isPresented: .init(
 58                get: { pendingRemoval != nil },
 59                set: { if !$0 { pendingRemoval = nil } }
 60            ),
 61            titleVisibility: .visible
 62        ) {
 63            Button("Remove", role: .destructive) {
 64                guard let label = pendingRemoval else { return }
 65                pendingRemoval = nil
 66                Task { await model.remove(label.name) }
 67            }
 68            Button("Cancel", role: .cancel) { pendingRemoval = nil }
 69        } message: {
 70            // The command takes the label off every issue, not just the set.
 71            Text("This takes the label off every issue that carries it.")
 72        }
 73    }
 74}
 75
 76private struct LabelRow: View {
 77    let label: IssueLabel
 78    let color: Color
 79    let working: Bool
 80    let recolour: (String) -> Void
 81    let remove: () -> Void
 82
 83    var body: some View {
 84        HStack(spacing: 8) {
 85            GBChip(label.name, color)
 86            Text(label.issues == 1 ? "1 issue" : "\(label.issues) issues")
 87                .font(.gbSans(.caption))
 88                .foregroundStyle(.secondary)
 89            Spacer()
 90            // An org label is managed with `org label`; the repository's
 91            // own commands refuse it, so it gets the mark and no menu.
 92            if label.org {
 93                GBChip("org", .secondary)
 94            } else {
 95                Menu {
 96                    ForEach(LabelPalette.palette, id: \.self) { hex in
 97                        Button(hex) { recolour(String(hex.dropFirst())) }
 98                    }
 99                    Button("Clear colour") { recolour("") }
100                    Button("Remove", role: .destructive) { remove() }
101                } label: {
102                    Image(systemName: "ellipsis.circle")
103                }
104                .disabled(working)
105            }
106        }
107    }
108}