gitbay/Views/Shared/ComposeSheet.swift
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}