| @@ -0,0 +1,102 @@ |
| 1 | import SwiftUI |
| 2 | |
| 3 | /// A repository's labels: their colour, how many issues carry each, and |
| 4 | /// the controls to create, recolour and remove them. |
| 5 | struct 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 | |
| 76 | private 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 | Menu { |
| 91 | ForEach(LabelPalette.palette, id: \.self) { hex in |
| 92 | Button(hex) { recolour(String(hex.dropFirst())) } |
| 93 | } |
| 94 | Button("Clear colour") { recolour("") } |
| 95 | Button("Remove", role: .destructive) { remove() } |
| 96 | } label: { |
| 97 | Image(systemName: "ellipsis.circle") |
| 98 | } |
| 99 | .disabled(working) |
| 100 | } |
| 101 | } |
| 102 | } |