188 lines
6.7 KiB
Swift
188 lines
6.7 KiB
Swift
//
|
|
// ProjectFormView.swift
|
|
// Snail
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
enum ProjectFormMode: Identifiable {
|
|
case create
|
|
case edit(Project)
|
|
|
|
var id: String {
|
|
switch self {
|
|
case .create: "create"
|
|
case .edit(let project): project.id
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Форма создания и изменения проекта. Ключ задаётся только при создании.
|
|
struct ProjectFormView: View {
|
|
@Environment(\.dismiss) private var dismiss
|
|
|
|
let mode: ProjectFormMode
|
|
let onSubmit: (_ key: String, _ name: String) async throws -> Void
|
|
|
|
/// Ограничения совпадают с серверными (ProjectName.MAX_LENGTH) и форматом номеров задач KEY-1.
|
|
private static let maxNameLength = 100
|
|
private static let maxKeyLength = 10
|
|
|
|
private enum Field {
|
|
case name
|
|
case key
|
|
}
|
|
|
|
@State private var name: String
|
|
@State private var key: String
|
|
@State private var isSaving = false
|
|
@State private var errorMessage: String?
|
|
@FocusState private var focusedField: Field?
|
|
|
|
init(mode: ProjectFormMode, onSubmit: @escaping (_ key: String, _ name: String) async throws -> Void) {
|
|
self.mode = mode
|
|
self.onSubmit = onSubmit
|
|
switch mode {
|
|
case .create:
|
|
_name = State(initialValue: "")
|
|
_key = State(initialValue: "")
|
|
case .edit(let project):
|
|
_name = State(initialValue: project.name)
|
|
_key = State(initialValue: project.key)
|
|
}
|
|
}
|
|
|
|
private var isCreating: Bool {
|
|
if case .create = mode { true } else { false }
|
|
}
|
|
|
|
private var trimmedName: String {
|
|
name.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
}
|
|
|
|
private var isKeyValid: Bool {
|
|
key.wholeMatch(of: /[A-Z][A-Z0-9]{1,9}/) != nil
|
|
}
|
|
|
|
private var isNameValid: Bool {
|
|
!trimmedName.isEmpty && trimmedName.count <= Self.maxNameLength
|
|
}
|
|
|
|
private var hasChanges: Bool {
|
|
switch mode {
|
|
case .create: !trimmedName.isEmpty || !key.isEmpty
|
|
case .edit(let project): trimmedName != project.name
|
|
}
|
|
}
|
|
|
|
private var canSubmit: Bool {
|
|
isNameValid && (isCreating ? isKeyValid : hasChanges) && !isSaving
|
|
}
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
Form {
|
|
Section {
|
|
TextField("Название", text: $name, prompt: Text("Например, Мобильное приложение"))
|
|
.textInputAutocapitalization(.sentences)
|
|
.submitLabel(isCreating ? .next : .done)
|
|
.focused($focusedField, equals: .name)
|
|
.onSubmit {
|
|
if isCreating {
|
|
focusedField = .key
|
|
} else {
|
|
submit()
|
|
}
|
|
}
|
|
} header: {
|
|
Text("Название")
|
|
} footer: {
|
|
if trimmedName.count > Self.maxNameLength {
|
|
Text("Не больше \(Self.maxNameLength) символов.")
|
|
.foregroundStyle(.red)
|
|
}
|
|
}
|
|
|
|
Section {
|
|
if isCreating {
|
|
TextField("Ключ", text: $key, prompt: Text(verbatim: "APP"))
|
|
.font(.body.monospaced())
|
|
.textInputAutocapitalization(.characters)
|
|
.autocorrectionDisabled()
|
|
.keyboardType(.asciiCapable)
|
|
.submitLabel(.done)
|
|
.focused($focusedField, equals: .key)
|
|
.onSubmit(submit)
|
|
.onChange(of: key) { _, newValue in
|
|
let normalized = String(
|
|
newValue.uppercased()
|
|
.filter { ("A"..."Z").contains($0) || ("0"..."9").contains($0) }
|
|
.prefix(Self.maxKeyLength)
|
|
)
|
|
if normalized != newValue { key = normalized }
|
|
}
|
|
} else {
|
|
LabeledContent("Ключ") {
|
|
Text(key).monospaced()
|
|
}
|
|
}
|
|
} header: {
|
|
Text("Ключ")
|
|
} footer: {
|
|
if isCreating {
|
|
Text("От 2 до \(Self.maxKeyLength) латинских букв и цифр, начинается с буквы. Используется в номерах задач — \(key.isEmpty ? "APP" : key)-1. Изменить ключ потом нельзя.")
|
|
} else {
|
|
Text("Ключ используется в номерах задач и не меняется.")
|
|
}
|
|
}
|
|
|
|
if let errorMessage {
|
|
Section {
|
|
Label(errorMessage, systemImage: "exclamationmark.circle")
|
|
.foregroundStyle(.red)
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle(isCreating ? "Новый проект" : "Изменить проект")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.disabled(isSaving)
|
|
.interactiveDismissDisabled(hasChanges || isSaving)
|
|
.toolbar {
|
|
ToolbarItem(placement: .cancellationAction) {
|
|
Button("Отмена", role: .cancel) { dismiss() }
|
|
.disabled(isSaving)
|
|
}
|
|
ToolbarItem(placement: .confirmationAction) {
|
|
if isSaving {
|
|
ProgressView()
|
|
} else {
|
|
Button(isCreating ? "Создать" : "Сохранить", action: submit)
|
|
.disabled(!canSubmit)
|
|
}
|
|
}
|
|
}
|
|
.onAppear { focusedField = .name }
|
|
}
|
|
}
|
|
|
|
private func submit() {
|
|
guard canSubmit else { return }
|
|
isSaving = true
|
|
errorMessage = nil
|
|
Task {
|
|
defer { isSaving = false }
|
|
do {
|
|
try await onSubmit(key, trimmedName)
|
|
dismiss()
|
|
} catch {
|
|
errorMessage = error.localizedDescription
|
|
AccessibilityNotification.Announcement(error.localizedDescription).post()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#Preview("Создание") {
|
|
ProjectFormView(mode: .create) { _, _ in }
|
|
}
|