gitbay/Views/Issues/LabelListView.swift

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

120 lines · 4562 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                    .disabled(
 41                        newLabel.trimmingCharacters(in: .whitespaces).isEmpty || model.working
 42                    )
 43                    .accessibilityIdentifier("label-create-submit")
 44                }
 45            }
 46
 47            ForEach(model.state.value ?? []) { label in
 48                // A repository sees its org's labels but cannot change
 49                // them; the org's own screen can.
 50                LabelRow(label: label, color: colors.color(label.name),
 51                         manageable: !label.org || model.owner.isOrg, working: model.working) {
 52                    hex in
 53                    Task { await model.setLabel(label.name, color: hex) }
 54                } remove: {
 55                    pendingRemoval = label
 56                }
 57            }
 58        }
 59        .overlay { LoadStateOverlay(state: model.state) }
 60        .navigationTitle("Labels")
 61        .navigationBarTitleDisplayMode(.inline)
 62        .task { await model.load() }
 63        .refreshable { await model.load() }
 64        .confirmationDialog(
 65            "Remove \(pendingRemoval?.name ?? "")?",
 66            isPresented: .init(
 67                get: { pendingRemoval != nil },
 68                set: { if !$0 { pendingRemoval = nil } }
 69            ),
 70            titleVisibility: .visible
 71        ) {
 72            Button("Remove", role: .destructive) {
 73                guard let label = pendingRemoval else { return }
 74                pendingRemoval = nil
 75                Task { await model.remove(label.name) }
 76            }
 77            Button("Cancel", role: .cancel) { pendingRemoval = nil }
 78        } message: {
 79            // The command takes the label off every issue, not just the set.
 80            Text(model.owner.isOrg
 81                ? "This takes the label off every issue in every repository under the org."
 82                : "This takes the label off every issue that carries it.")
 83        }
 84    }
 85}
 86
 87private struct LabelRow: View {
 88    let label: IssueLabel
 89    let color: Color
 90    let manageable: Bool
 91    let working: Bool
 92    let recolour: (String) -> Void
 93    let remove: () -> Void
 94
 95    var body: some View {
 96        HStack(spacing: 8) {
 97            GBChip(label.name, color)
 98            Text(label.issues == 1 ? "1 issue" : "\(label.issues) issues")
 99                .font(.gbSans(.caption))
100                .foregroundStyle(.secondary)
101            Spacer()
102            // An org label is managed with `org label`; the repository's
103            // own commands refuse it, so it gets the mark and no menu.
104            if !manageable {
105                GBChip("org", .secondary)
106            } else {
107                Menu {
108                    ForEach(LabelPalette.palette, id: \.self) { hex in
109                        Button(hex) { recolour(String(hex.dropFirst())) }
110                    }
111                    Button("Clear colour") { recolour("") }
112                    Button("Remove", role: .destructive) { remove() }
113                } label: {
114                    Image(systemName: "ellipsis.circle")
115                }
116                .disabled(working)
117            }
118        }
119    }
120}