diff options
Diffstat (limited to 'Rune/Views/Domains')
| -rw-r--r-- | Rune/Views/Domains/DomainDetailView.swift | 28 | ||||
| -rw-r--r-- | Rune/Views/Domains/DomainEditView.swift | 117 | ||||
| -rw-r--r-- | Rune/Views/Domains/DomainListView.swift | 71 | ||||
| -rw-r--r-- | Rune/Views/Domains/ForwardAddView.swift | 111 | ||||
| -rw-r--r-- | Rune/Views/Domains/ForwardListView.swift | 171 | ||||
| -rw-r--r-- | Rune/Views/Domains/RecordAddView.swift | 37 | ||||
| -rw-r--r-- | Rune/Views/Domains/RecordEditView.swift | 51 | ||||
| -rw-r--r-- | Rune/Views/Domains/RecordListView.swift | 55 |
8 files changed, 551 insertions, 90 deletions
diff --git a/Rune/Views/Domains/DomainDetailView.swift b/Rune/Views/Domains/DomainDetailView.swift index dbb90a1..b4ee00d 100644 --- a/Rune/Views/Domains/DomainDetailView.swift +++ b/Rune/Views/Domains/DomainDetailView.swift @@ -25,6 +25,12 @@ struct DomainDetailView: View { DetailRow(label: "Nameservers", value: nameserverText(domain.nameservers)) } + Section("Email") { + NavigationLink("Forwards") { + ForwardListView(domainName: domain.name, viewModel: viewModel, client: client) + } + } + Section("DNS") { NavigationLink("Records") { RecordListView(domainName: domain.name, viewModel: viewModel, client: client) @@ -49,7 +55,12 @@ struct DomainDetailView: View { .alert("API Error", isPresented: errorBinding) { Button("OK", role: .cancel) {} } message: { - Text(viewModel.errorMessage ?? "") + Text(viewModel.detailErrorMessage ?? "") + } + .alert("Request Failed", isPresented: mutationErrorBinding) { + Button("OK", role: .cancel) {} + } message: { + Text(viewModel.mutationErrorMessage ?? "") } } @@ -88,10 +99,21 @@ struct DomainDetailView: View { private var errorBinding: Binding<Bool> { Binding( - get: { viewModel.errorMessage != nil }, + get: { viewModel.detailErrorMessage != nil }, + set: { newValue in + if !newValue { + viewModel.detailErrorMessage = nil + } + } + ) + } + + private var mutationErrorBinding: Binding<Bool> { + Binding( + get: { viewModel.mutationErrorMessage != nil }, set: { newValue in if !newValue { - viewModel.errorMessage = nil + viewModel.dismissMutationError() } } ) diff --git a/Rune/Views/Domains/DomainEditView.swift b/Rune/Views/Domains/DomainEditView.swift index 3f197e3..0b3b1f8 100644 --- a/Rune/Views/Domains/DomainEditView.swift +++ b/Rune/Views/Domains/DomainEditView.swift @@ -8,10 +8,15 @@ struct DomainEditView: View { @Environment(\.dismiss) private var dismiss @State private var autorenew: Bool + @State private var autorenewDirty = false @State private var mailforwarding: Bool + @State private var mailforwardingDirty = false @State private var dnssec: Bool + @State private var dnssecDirty = false @State private var lock: Bool + @State private var lockDirty = false @State private var nameserversText: String + @State private var nameserversDirty = false @State private var localErrorMessage: String? init(domain: Domain, viewModel: DomainViewModel, client: NjallaClient) { @@ -28,14 +33,14 @@ struct DomainEditView: View { var body: some View { Form { Section("Settings") { - Toggle("Autorenew", isOn: $autorenew) - Toggle("Mail Forwarding", isOn: $mailforwarding) - Toggle("DNSSEC", isOn: $dnssec) - Toggle("Registrar Lock", isOn: $lock) + Toggle("Autorenew", isOn: dirtyBinding(for: $autorenew, dirty: $autorenewDirty, original: originalAutorenew)) + Toggle("Mail Forwarding", isOn: dirtyBinding(for: $mailforwarding, dirty: $mailforwardingDirty, original: originalMailForwarding)) + Toggle("DNSSEC", isOn: dirtyBinding(for: $dnssec, dirty: $dnssecDirty, original: originalDNSSEC)) + Toggle("Registrar Lock", isOn: dirtyBinding(for: $lock, dirty: $lockDirty, original: originalLock)) } Section { - TextEditor(text: $nameserversText) + TextEditor(text: nameserversBinding) .frame(minHeight: 120) } header: { Text("Nameservers") @@ -49,12 +54,27 @@ struct DomainEditView: View { await save() } } - .disabled(viewModel.isSaving) + .disabled(viewModel.isSaving || !request.hasChanges) } } .navigationTitle("Edit Domain") .navigationBarTitleDisplayMode(.inline) - .alert("API Error", isPresented: localErrorBinding) { + .overlay { + if viewModel.isSaving { + ProgressView() + .controlSize(.large) + } + } + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel", role: .cancel) { + dismiss() + } + .disabled(viewModel.isSaving) + } + } + .interactiveDismissDisabled(viewModel.isSaving) + .alert("Request Failed", isPresented: localErrorBinding) { Button("OK", role: .cancel) {} } message: { Text(localErrorMessage ?? "") @@ -62,16 +82,16 @@ struct DomainEditView: View { } private func save() async { - let request = DomainUpdateRequest( - autorenew: autorenew, - mailforwarding: mailforwarding, - dnssec: dnssec, - lock: lock, - nameservers: nameserversText - .split(whereSeparator: \.isNewline) - .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } - .filter { !$0.isEmpty } - ) + guard !viewModel.isSaving else { + return + } + + let request = request + + guard request.hasChanges else { + dismiss() + return + } do { try await viewModel.updateDomain(named: domain.name, request: request, client: client) @@ -82,7 +102,7 @@ struct DomainEditView: View { if (error as? URLError)?.code == .cancelled { return } - localErrorMessage = error.localizedDescription + localErrorMessage = error.userFacingMessage } } @@ -96,4 +116,65 @@ struct DomainEditView: View { } ) } + + private var originalAutorenew: Bool { + domain.autorenew ?? false + } + + private var originalMailForwarding: Bool { + domain.mailforwarding ?? false + } + + private var originalDNSSEC: Bool { + domain.dnssec ?? false + } + + private var originalLock: Bool { + domain.lock ?? false + } + + private var originalNameservers: [String] { + normalizedNameservers(from: (domain.nameservers ?? []).joined(separator: "\n")) + } + + private var request: DomainUpdateRequest { + DomainUpdateRequest( + autorenew: autorenewDirty ? autorenew : nil, + mailforwarding: mailforwardingDirty ? mailforwarding : nil, + dnssec: dnssecDirty ? dnssec : nil, + lock: lockDirty ? lock : nil, + nameservers: nameserversDirty ? normalizedNameservers(from: nameserversText) : nil + ) + } + + private var nameserversBinding: Binding<String> { + Binding( + get: { nameserversText }, + set: { newValue in + nameserversText = newValue + nameserversDirty = normalizedNameservers(from: newValue) != originalNameservers + } + ) + } + + private func dirtyBinding( + for value: Binding<Bool>, + dirty: Binding<Bool>, + original: Bool + ) -> Binding<Bool> { + Binding( + get: { value.wrappedValue }, + set: { newValue in + value.wrappedValue = newValue + dirty.wrappedValue = newValue != original + } + ) + } + + private func normalizedNameservers(from text: String) -> [String] { + text + .split(whereSeparator: \.isNewline) + .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } + .filter { !$0.isEmpty } + } } diff --git a/Rune/Views/Domains/DomainListView.swift b/Rune/Views/Domains/DomainListView.swift index d728bd7..68f2ede 100644 --- a/Rune/Views/Domains/DomainListView.swift +++ b/Rune/Views/Domains/DomainListView.swift @@ -15,43 +15,58 @@ struct DomainListView: View { } .navigationTitle("Domains") } - .alert("API Error", isPresented: errorBinding) { - Button("OK", role: .cancel) {} - } message: { - Text(viewModel.errorMessage ?? "") - } } @ViewBuilder private func content(client: NjallaClient) -> some View { - if viewModel.isLoadingDomains && viewModel.domains.isEmpty { - ProgressView() - } else if viewModel.domains.isEmpty { - ContentUnavailableView("No Domains", systemImage: "globe", description: Text("No domains found on this account.")) - } else { - List(viewModel.domains) { domain in - NavigationLink { - DomainDetailView(domainName: domain.name, viewModel: viewModel, client: client) - } label: { - DomainRow(domain: domain) + List { + if let errorMessage = viewModel.domainsErrorMessage { + Section { + InlineErrorView(message: errorMessage, retryTitle: "Retry Domains") { + Task { + await viewModel.loadDomains(client: client) + } + } + .listRowInsets(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16)) } } - .listStyle(.insetGrouped) - .refreshable { - await viewModel.loadDomains(client: client) - } - } - } - private var errorBinding: Binding<Bool> { - Binding( - get: { viewModel.errorMessage != nil }, - set: { newValue in - if !newValue { - viewModel.errorMessage = nil + if (!viewModel.hasLoadedDomains || viewModel.isLoadingDomains) && viewModel.domains.isEmpty { + Section { + HStack { + Spacer() + ProgressView("Loading Domains") + Spacer() + } + } + } else if viewModel.domains.isEmpty { + Section { + ContentUnavailableView( + "No Domains", + systemImage: "globe", + description: Text("No domains found. Pull to refresh after domains are added to this account.") + ) + } + } else { + ForEach(viewModel.domains) { domain in + NavigationLink { + DomainDetailView(domainName: domain.name, viewModel: viewModel, client: client) + } label: { + DomainRow(domain: domain) + } } } - ) + } + .listStyle(.insetGrouped) + .refreshable { + await viewModel.loadDomains(client: client) + } + .overlay(alignment: .top) { + if viewModel.isLoadingDomains && !viewModel.domains.isEmpty { + ProgressView() + .padding(.top, 8) + } + } } } diff --git a/Rune/Views/Domains/ForwardAddView.swift b/Rune/Views/Domains/ForwardAddView.swift new file mode 100644 index 0000000..222ab86 --- /dev/null +++ b/Rune/Views/Domains/ForwardAddView.swift @@ -0,0 +1,111 @@ +import SwiftUI + +struct ForwardAddView: View { + let domainName: String + @ObservedObject var viewModel: DomainViewModel + let client: NjallaClient + + @Environment(\.dismiss) private var dismiss + + @State private var from = "" + @State private var to = "" + + var body: some View { + Form { + Section { + TextField("From", text: $from) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + TextField("To", text: $to) + .textInputAutocapitalization(.never) + .keyboardType(.emailAddress) + .autocorrectionDisabled() + } header: { + Text("Forward") + } footer: { + Text("Creates \(trimmedFrom)@\(domainName) -> \(trimmedTo)") + } + + Section { + Button("Save") { + Task { + await save() + } + } + .disabled(viewModel.isSaving || !canSubmit) + } + } + .navigationTitle("Add Forward") + .navigationBarTitleDisplayMode(.inline) + .overlay { + if viewModel.isSaving { + ProgressView() + .controlSize(.large) + } + } + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel", role: .cancel) { + dismiss() + } + .disabled(viewModel.isSaving) + } + } + .interactiveDismissDisabled(viewModel.isSaving) + .alert("Request Failed", isPresented: mutationErrorBinding) { + Button("OK", role: .cancel) {} + } message: { + Text(viewModel.mutationErrorMessage ?? "") + } + } + + private var trimmedFrom: String { + from.trimmingCharacters(in: .whitespacesAndNewlines) + } + + private var trimmedTo: String { + to.trimmingCharacters(in: .whitespacesAndNewlines) + } + + private var canSubmit: Bool { + !trimmedFrom.isEmpty && !trimmedTo.isEmpty + } + + private func save() async { + guard !viewModel.isSaving, canSubmit else { + return + } + + let forward = EmailForward(domain: domainName, from: trimmedFrom, to: trimmedTo) + + debugLog("Creating forward \(forward.from)@\(forward.domain) -> \(forward.to)") + do { + try await viewModel.addForward(forward, client: client) + debugLog("Created forward \(forward.from)@\(forward.domain) -> \(forward.to)") + dismiss() + } catch is CancellationError { + debugLog("Create cancelled for \(forward.from)@\(forward.domain) -> \(forward.to)") + return + } catch { + debugLog("Create failed for \(forward.from)@\(forward.domain) -> \(forward.to): \(error.localizedDescription)") + return + } + } + + private var mutationErrorBinding: Binding<Bool> { + Binding( + get: { viewModel.mutationErrorMessage != nil }, + set: { newValue in + if !newValue { + viewModel.dismissMutationError() + } + } + ) + } + + private func debugLog(_ message: String) { + #if DEBUG + debugPrint("[ForwardAddView]", message) + #endif + } +} diff --git a/Rune/Views/Domains/ForwardListView.swift b/Rune/Views/Domains/ForwardListView.swift new file mode 100644 index 0000000..4e20ab2 --- /dev/null +++ b/Rune/Views/Domains/ForwardListView.swift @@ -0,0 +1,171 @@ +import SwiftUI + +struct ForwardListView: View { + let domainName: String + @ObservedObject var viewModel: DomainViewModel + let client: NjallaClient + + @State private var showingAddForward = false + @State private var forwardPendingDeletion: EmailForward? + + var body: some View { + List { + if let errorMessage = viewModel.forwardsErrorMessage { + Section { + InlineErrorView(message: errorMessage, retryTitle: "Retry Forwards") { + Task { + await viewModel.loadForwards(for: domainName, client: client) + } + } + .listRowInsets(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16)) + } + } + + if viewModel.isLoadingForwards && viewModel.forwards.isEmpty { + Section { + HStack { + Spacer() + ProgressView("Loading Forwards") + Spacer() + } + } + } else if viewModel.forwards.isEmpty { + Section { + ContentUnavailableView( + "No Forwards", + systemImage: "envelope", + description: Text("No email forwards are configured for this domain.") + ) + } + } else { + ForEach(viewModel.forwards) { forward in + VStack(alignment: .leading, spacing: 4) { + Text("@\(forward.from)") + .font(.headline) + Text(forward.to) + .font(.subheadline) + .foregroundStyle(.secondary) + } + .padding(.vertical, 4) + .swipeActions { + Button("Delete", role: .destructive) { + guard !viewModel.isSaving else { return } + forwardPendingDeletion = forward + } + } + .contextMenu { + Button("Delete Forward", role: .destructive) { + forwardPendingDeletion = forward + } + } + .disabled(viewModel.isSaving) + } + } + } + .listStyle(.insetGrouped) + .navigationTitle("Forwards") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + Button { + showingAddForward = true + } label: { + Label("Add Forward", systemImage: "plus") + } + .disabled(viewModel.isSaving) + } + .sheet(isPresented: $showingAddForward) { + NavigationStack { + ForwardAddView(domainName: domainName, viewModel: viewModel, client: client) + } + } + .task { + debugLog("Loading forwards for \(domainName)") + await viewModel.loadForwards(for: domainName, client: client) + debugLog("Loaded \(viewModel.forwards.count) forwards for \(domainName)") + } + .refreshable { + debugLog("Refreshing forwards for \(domainName)") + await viewModel.loadForwards(for: domainName, client: client) + debugLog("Refresh complete with \(viewModel.forwards.count) forwards for \(domainName)") + } + .overlay(alignment: .top) { + if viewModel.isLoadingForwards && !viewModel.forwards.isEmpty { + ProgressView() + .padding(.top, 8) + } + } + .alert(deleteAlertTitle, isPresented: deleteBinding) { + Button("Delete Forward", role: .destructive) { + guard let forwardPendingDeletion else { return } + Task { + await delete(forwardPendingDeletion) + } + } + Button("Cancel", role: .cancel) { + forwardPendingDeletion = nil + } + } message: { + Text("Delete the forward from \(forwardPendingDeletion?.from ?? "") to \(forwardPendingDeletion?.to ?? "")?") + } + .alert("Request Failed", isPresented: mutationErrorBinding) { + Button("OK", role: .cancel) {} + } message: { + Text(viewModel.mutationErrorMessage ?? "") + } + } + + private func delete(_ forward: EmailForward) async { + guard !viewModel.isSaving else { + return + } + + debugLog("Deleting forward \(forward.from)@\(forward.domain) -> \(forward.to)") + do { + try await viewModel.removeForward(forward, client: client) + debugLog("Deleted forward \(forward.from)@\(forward.domain) -> \(forward.to)") + forwardPendingDeletion = nil + } catch is CancellationError { + debugLog("Delete cancelled for \(forward.from)@\(forward.domain) -> \(forward.to)") + return + } catch { + debugLog("Delete failed for \(forward.from)@\(forward.domain) -> \(forward.to): \(error.localizedDescription)") + return + } + } + + private var deleteAlertTitle: String { + guard let forwardPendingDeletion else { + return "" + } + + return "Delete forward \(forwardPendingDeletion.from)@\(domainName)?" + } + + private var deleteBinding: Binding<Bool> { + Binding( + get: { forwardPendingDeletion != nil }, + set: { newValue in + if !newValue { + forwardPendingDeletion = nil + } + } + ) + } + + private var mutationErrorBinding: Binding<Bool> { + Binding( + get: { viewModel.mutationErrorMessage != nil }, + set: { newValue in + if !newValue { + viewModel.dismissMutationError() + } + } + ) + } + + private func debugLog(_ message: String) { + #if DEBUG + debugPrint("[ForwardListView]", message) + #endif + } +} diff --git a/Rune/Views/Domains/RecordAddView.swift b/Rune/Views/Domains/RecordAddView.swift index 8dfa81d..2cc5dde 100644 --- a/Rune/Views/Domains/RecordAddView.swift +++ b/Rune/Views/Domains/RecordAddView.swift @@ -8,7 +8,6 @@ struct RecordAddView: View { @Environment(\.dismiss) private var dismiss @State private var draft = DNSRecordDraft() - @State private var localErrorMessage: String? var body: some View { Form { DNSRecordFormSections(draft: $draft) @@ -24,44 +23,54 @@ struct RecordAddView: View { } .navigationTitle("Add Record") .navigationBarTitleDisplayMode(.inline) + .overlay { + if viewModel.isSaving { + ProgressView() + .controlSize(.large) + } + } .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel", role: .cancel) { dismiss() } + .disabled(viewModel.isSaving) } } + .interactiveDismissDisabled(viewModel.isSaving) .onChange(of: draft.type) { oldValue, newValue in guard oldValue != newValue else { return } draft.resetTypeSpecificFields() } - .alert("API Error", isPresented: localErrorBinding) { + .alert("Request Failed", isPresented: mutationErrorBinding) { Button("OK", role: .cancel) {} } message: { - Text(localErrorMessage ?? "") + Text(viewModel.mutationErrorMessage ?? "") } } private func save() async { + guard !viewModel.isSaving else { + return + } + do { try await viewModel.addRecord(for: domainName, draft: draft, client: client) + draft = DNSRecordDraft() dismiss() } catch is CancellationError { return } catch { - if (error as? URLError)?.code == .cancelled { - return - } - localErrorMessage = error.localizedDescription + return } } - private var localErrorBinding: Binding<Bool> { + private var mutationErrorBinding: Binding<Bool> { Binding( - get: { localErrorMessage != nil }, + get: { viewModel.mutationErrorMessage != nil }, set: { newValue in if !newValue { - localErrorMessage = nil + viewModel.dismissMutationError() } } ) @@ -94,8 +103,12 @@ struct DNSRecordFormSections: View { if draft.type.usesTTL { Section("TTL") { - TextField("TTL", text: $draft.ttl) - .keyboardType(.numberPad) + Picker("TTL", selection: $draft.ttlSeconds) { + ForEach(draft.ttlOptions) { option in + Text(option.isCustom ? "Custom (\(option.label))" : option.label) + .tag(option.seconds) + } + } } } diff --git a/Rune/Views/Domains/RecordEditView.swift b/Rune/Views/Domains/RecordEditView.swift index e0c0fde..fceaaba 100644 --- a/Rune/Views/Domains/RecordEditView.swift +++ b/Rune/Views/Domains/RecordEditView.swift @@ -10,7 +10,6 @@ struct RecordEditView: View { @State private var draft: DNSRecordDraft @State private var showingDeleteConfirmation = false - @State private var localErrorMessage: String? init(domainName: String, record: DNSRecord, viewModel: DomainViewModel, client: NjallaClient) { self.domainName = domainName @@ -35,71 +34,85 @@ struct RecordEditView: View { Section { Button("Delete Record", role: .destructive) { + guard !viewModel.isSaving else { return } showingDeleteConfirmation = true } .foregroundStyle(.red) + .disabled(viewModel.isSaving) } } .navigationTitle(record.name) .navigationBarTitleDisplayMode(.inline) + .overlay { + if viewModel.isSaving { + ProgressView() + .controlSize(.large) + } + } + .interactiveDismissDisabled(viewModel.isSaving) .onChange(of: draft.type) { oldValue, newValue in guard oldValue != newValue else { return } draft.resetTypeSpecificFields() } - .confirmationDialog( - "Delete \(record.type) record \(record.name)?", - isPresented: $showingDeleteConfirmation, - titleVisibility: .visible - ) { + .alert(deleteAlertTitle, isPresented: $showingDeleteConfirmation) { Button("Delete Record", role: .destructive) { Task { await deleteRecord() } } + Button("Cancel", role: .cancel) {} + } message: { + Text("This action cannot be undone.") } - .alert("API Error", isPresented: localErrorBinding) { + .alert("Request Failed", isPresented: mutationErrorBinding) { Button("OK", role: .cancel) {} } message: { - Text(localErrorMessage ?? "") + Text(viewModel.mutationErrorMessage ?? "") } } private func save() async { + guard !viewModel.isSaving else { + return + } + do { try await viewModel.editRecord(for: domainName, recordID: record.id, draft: draft, client: client) dismiss() } catch is CancellationError { return } catch { - if (error as? URLError)?.code == .cancelled { - return - } - localErrorMessage = error.localizedDescription + return } } private func deleteRecord() async { + guard !viewModel.isSaving else { + return + } + do { try await viewModel.removeRecord(record, client: client) dismiss() } catch is CancellationError { return } catch { - if (error as? URLError)?.code == .cancelled { - return - } - localErrorMessage = error.localizedDescription + return } } - private var localErrorBinding: Binding<Bool> { + private var mutationErrorBinding: Binding<Bool> { Binding( - get: { localErrorMessage != nil }, + get: { viewModel.mutationErrorMessage != nil }, set: { newValue in if !newValue { - localErrorMessage = nil + viewModel.dismissMutationError() } } ) } + + private var deleteAlertTitle: String { + "Delete \(record.type) record \(record.name)?" + } } diff --git a/Rune/Views/Domains/RecordListView.swift b/Rune/Views/Domains/RecordListView.swift index 67a4140..6c40ab0 100644 --- a/Rune/Views/Domains/RecordListView.swift +++ b/Rune/Views/Domains/RecordListView.swift @@ -8,22 +8,45 @@ struct RecordListView: View { @State private var showingAddRecord = false var body: some View { - Group { + List { + if let errorMessage = viewModel.recordsErrorMessage { + Section { + InlineErrorView(message: errorMessage, retryTitle: "Retry Records") { + Task { + await viewModel.loadRecords(for: domainName, client: client) + } + } + .listRowInsets(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16)) + } + } + if viewModel.isLoadingRecords && viewModel.records.isEmpty { - ProgressView() + Section { + HStack { + Spacer() + ProgressView("Loading Records") + Spacer() + } + } } else if viewModel.records.isEmpty { - ContentUnavailableView("No Records", systemImage: "list.bullet", description: Text("No DNS records for this domain.")) + Section { + ContentUnavailableView( + "No Records", + systemImage: "list.bullet", + description: Text("No DNS records for this domain yet. Add a record to get started.") + ) + } } else { - List(viewModel.records) { record in + ForEach(viewModel.records) { record in NavigationLink { RecordEditView(domainName: domainName, record: record, viewModel: viewModel, client: client) } label: { RecordRow(record: record) } } - .listStyle(.insetGrouped) } } + .listStyle(.insetGrouped) .navigationTitle("DNS Records") .navigationBarTitleDisplayMode(.inline) .toolbar { @@ -41,22 +64,34 @@ struct RecordListView: View { .task { await viewModel.loadRecords(for: domainName, client: client) } + .onAppear { + viewModel.startAutoRefreshRecords(for: domainName, client: client) + } + .onDisappear { + viewModel.stopAutoRefreshRecords() + } .refreshable { await viewModel.loadRecords(for: domainName, client: client) } - .alert("API Error", isPresented: errorBinding) { + .overlay(alignment: .top) { + if viewModel.isLoadingRecords && !viewModel.records.isEmpty { + ProgressView() + .padding(.top, 8) + } + } + .alert("Request Failed", isPresented: mutationErrorBinding) { Button("OK", role: .cancel) {} } message: { - Text(viewModel.errorMessage ?? "") + Text(viewModel.mutationErrorMessage ?? "") } } - private var errorBinding: Binding<Bool> { + private var mutationErrorBinding: Binding<Bool> { Binding( - get: { viewModel.errorMessage != nil }, + get: { viewModel.mutationErrorMessage != nil }, set: { newValue in if !newValue { - viewModel.errorMessage = nil + viewModel.dismissMutationError() } } ) |
