gitbay/Views/Shared/ComposeSheet.swift

36835f99e6d463f3e18c5c3fc3562bb92513f52b
gitbay-ios/gitbay/Views/Shared/ComposeSheet.swift history · blame · raw

104 lines · 4228 bytes

1 symbol in this file
  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                    MarkupPreview(text: bodyText, format: format, identifier: "compose-preview")
 77                }
 78                if let errorMessage {
 79                    Section {
 80                        GBNotice(errorMessage)
 81                    }
 82                }
 83            }
 84            .scrollDismissesKeyboard(.interactively)
 85            .navigationTitle(heading)
 86            .navigationBarTitleDisplayMode(.inline)
 87            .toolbar {
 88                ToolbarItem(placement: .cancellationAction) {
 89                    Button("Cancel") { dismiss() }
 90                }
 91                ToolbarItem(placement: .confirmationAction) {
 92                    if working {
 93                        ProgressView()
 94                    } else {
 95                        Button(submitLabel, action: onSubmit)
 96                            .disabled(title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
 97                            .accessibilityIdentifier("compose-submit")
 98                    }
 99                }
100            }
101            .interactiveDismissDisabled(working)
102        }
103    }
104}