import SwiftUI /// A repository's or an org's labels: their colour, how many issues /// carry each, and the controls to create, recolour and remove them. struct LabelListView: View { @State private var model: LabelListViewModel @State private var newLabel = "" @State private var pendingRemoval: IssueLabel? init(client: GitbayClient, repo: String) { self.init(client: client, owner: .repo(repo)) } init(client: GitbayClient, owner: LabelOwner) { _model = State(initialValue: LabelListViewModel(client: client, owner: owner)) } private var colors: LabelColors { LabelColors(model.state.value ?? []) } var body: some View { List { if let actionError = model.actionError { Section { GBNotice(actionError) } } Section("New label") { HStack { TextField("Label", text: $newLabel) .autocorrectionDisabled() .textInputAutocapitalization(.never) .accessibilityIdentifier("label-new-name") Button { let name = newLabel.trimmingCharacters(in: .whitespaces) newLabel = "" Task { await model.setLabel(name, color: nil) } } label: { Image(systemName: "plus.circle.fill") } .disabled( newLabel.trimmingCharacters(in: .whitespaces).isEmpty || model.working ) .accessibilityIdentifier("label-create-submit") } } ForEach(model.state.value ?? []) { label in // A repository sees its org's labels but cannot change // them; the org's own screen can. LabelRow(label: label, color: colors.color(label.name), manageable: !label.org || model.owner.isOrg, working: model.working) { hex in Task { await model.setLabel(label.name, color: hex) } } remove: { pendingRemoval = label } } } .overlay { LoadStateOverlay(state: model.state) } .navigationTitle("Labels") .navigationBarTitleDisplayMode(.inline) .task { await model.load() } .refreshable { await model.load() } .confirmationDialog( "Remove \(pendingRemoval?.name ?? "")?", isPresented: .init( get: { pendingRemoval != nil }, set: { if !$0 { pendingRemoval = nil } } ), titleVisibility: .visible ) { Button("Remove", role: .destructive) { guard let label = pendingRemoval else { return } pendingRemoval = nil Task { await model.remove(label.name) } } Button("Cancel", role: .cancel) { pendingRemoval = nil } } message: { // The command takes the label off every issue, not just the set. Text(model.owner.isOrg ? "This takes the label off every issue in every repository under the org." : "This takes the label off every issue that carries it.") } } } private struct LabelRow: View { let label: IssueLabel let color: Color let manageable: Bool let working: Bool let recolour: (String) -> Void let remove: () -> Void var body: some View { HStack(spacing: 8) { GBChip(label.name, color) Text(label.issues == 1 ? "1 issue" : "\(label.issues) issues") .font(.gbSans(.caption)) .foregroundStyle(.secondary) Spacer() // An org label is managed with `org label`; the repository's // own commands refuse it, so it gets the mark and no menu. if !manageable { GBChip("org", .secondary) } else { Menu { ForEach(LabelPalette.palette, id: \.self) { hex in Button(hex) { recolour(String(hex.dropFirst())) } } Button("Clear colour") { recolour("") } Button("Remove", role: .destructive) { remove() } } label: { Image(systemName: "ellipsis.circle") } .disabled(working) } } } }