gitbay/Views/Shared/ComposeSheet.swift

v1.4.0
gitbay-ios/gitbay/Views/Shared/ComposeSheet.swift history · blame · raw

102 lines · 4078 bytes

  1import SwiftUI
  2
  3/// Title-and-body editor shared by issue/MR create and edit sheets.
  4struct ComposeSheet: View {
  5
  6    let heading: String
  7    let submitLabel: String
  8    let working: Bool
  9    let errorMessage: String?
 10    @Binding var title: String
 11    @Binding var bodyText: String
 12    /// Held as a real `@Binding`, not `Binding<String>?`: an optional
 13    /// binding is not a dynamic property, so the sheet body never re-ran
 14    /// when the value changed and the picker kept whatever it first saw.
 15    @Binding var format: String
 16    let showsFormat: Bool
 17    /// Issue templates to start the body from; empty hides the menu.
 18    let templates: [IssueTemplate]
 19    let onSubmit: () -> Void
 20
 21    /// A nil `format` hides the picker entirely — the release editor has
 22    /// no parity row and passes nothing, leaving that call site untouched.
 23    init(heading: String, submitLabel: String, working: Bool, errorMessage: String?,
 24         title: Binding<String>, bodyText: Binding<String>,
 25         format: Binding<String>? = nil, templates: [IssueTemplate] = [],
 26         onSubmit: @escaping () -> Void) {
 27        self.heading = heading
 28        self.submitLabel = submitLabel
 29        self.working = working
 30        self.errorMessage = errorMessage
 31        _title = title
 32        _bodyText = bodyText
 33        _format = format ?? .constant("md")
 34        showsFormat = format != nil
 35        self.templates = templates
 36        self.onSubmit = onSubmit
 37    }
 38
 39    @Environment(\.dismiss) private var dismiss
 40
 41    var body: some View {
 42        NavigationStack {
 43            Form {
 44                Section("Title") {
 45                    TextField("Title", text: $title, axis: .vertical)
 46                        .lineLimit(1...3)
 47                        // Titles name code: paths, commands, branch names.
 48                        // Autocorrection rewrites them.
 49                        .autocorrectionDisabled()
 50                        .accessibilityIdentifier("compose-title")
 51                }
 52                Section("Body") {
 53                    if !templates.isEmpty {
 54                        Menu {
 55                            ForEach(templates) { template in
 56                                Button(template.name) { bodyText = template.body }
 57                            }
 58                        } label: {
 59                            Label("Template", systemImage: "doc.text")
 60                        }
 61                        .accessibilityIdentifier("compose-template-menu")
 62                    }
 63                    TextEditor(text: $bodyText)
 64                        .frame(minHeight: 160)
 65                        .font(.gbSans(.body))
 66                        .autocorrectionDisabled()
 67                        .accessibilityIdentifier("compose-body")
 68                    if showsFormat {
 69                        Picker("Format", selection: $format) {
 70                            Text("Markdown").tag("md")
 71                            Text("Org").tag("org")
 72                        }
 73                        .pickerStyle(.segmented)
 74                        .accessibilityIdentifier("compose-format")
 75                    }
 76                }
 77                if let errorMessage {
 78                    Section {
 79                        GBNotice(errorMessage)
 80                    }
 81                }
 82            }
 83            .navigationTitle(heading)
 84            .navigationBarTitleDisplayMode(.inline)
 85            .toolbar {
 86                ToolbarItem(placement: .cancellationAction) {
 87                    Button("Cancel") { dismiss() }
 88                }
 89                ToolbarItem(placement: .confirmationAction) {
 90                    if working {
 91                        ProgressView()
 92                    } else {
 93                        Button(submitLabel, action: onSubmit)
 94                            .disabled(title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
 95                            .accessibilityIdentifier("compose-submit")
 96                    }
 97                }
 98            }
 99            .interactiveDismissDisabled(working)
100        }
101    }
102}