gitbay/Views/Shared/ComposeSheet.swift

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

88 lines · 3419 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    let onSubmit: () -> Void
18
19    /// A nil `format` hides the picker entirely — the release editor has
20    /// no parity row and passes nothing, leaving that call site untouched.
21    init(heading: String, submitLabel: String, working: Bool, errorMessage: String?,
22         title: Binding<String>, bodyText: Binding<String>,
23         format: Binding<String>? = nil, onSubmit: @escaping () -> Void) {
24        self.heading = heading
25        self.submitLabel = submitLabel
26        self.working = working
27        self.errorMessage = errorMessage
28        _title = title
29        _bodyText = bodyText
30        _format = format ?? .constant("md")
31        showsFormat = format != nil
32        self.onSubmit = onSubmit
33    }
34
35    @Environment(\.dismiss) private var dismiss
36
37    var body: some View {
38        NavigationStack {
39            Form {
40                Section("Title") {
41                    TextField("Title", text: $title, axis: .vertical)
42                        .lineLimit(1...3)
43                        // Titles name code: paths, commands, branch names.
44                        // Autocorrection rewrites them.
45                        .autocorrectionDisabled()
46                        .accessibilityIdentifier("compose-title")
47                }
48                Section("Body") {
49                    TextEditor(text: $bodyText)
50                        .frame(minHeight: 160)
51                        .font(.gbSans(.body))
52                        .autocorrectionDisabled()
53                        .accessibilityIdentifier("compose-body")
54                    if showsFormat {
55                        Picker("Format", selection: $format) {
56                            Text("Markdown").tag("md")
57                            Text("Org").tag("org")
58                        }
59                        .pickerStyle(.segmented)
60                        .accessibilityIdentifier("compose-format")
61                    }
62                }
63                if let errorMessage {
64                    Section {
65                        GBNotice(errorMessage)
66                    }
67                }
68            }
69            .navigationTitle(heading)
70            .navigationBarTitleDisplayMode(.inline)
71            .toolbar {
72                ToolbarItem(placement: .cancellationAction) {
73                    Button("Cancel") { dismiss() }
74                }
75                ToolbarItem(placement: .confirmationAction) {
76                    if working {
77                        ProgressView()
78                    } else {
79                        Button(submitLabel, action: onSubmit)
80                            .disabled(title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
81                            .accessibilityIdentifier("compose-submit")
82                    }
83                }
84            }
85            .interactiveDismissDisabled(working)
86        }
87    }
88}