gitbay/Views/Issues/LabelListView.swift

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

122 lines · 4680 bytes

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