Validates url when adding a link

This commit is contained in:
2025-08-16 12:43:50 +10:00
parent d55fd33678
commit 022021014c
4 changed files with 130 additions and 1 deletions
@@ -120,6 +120,7 @@
/* Validation Messages */
"alias_already_exists" = "Alias already exists";
"alias_invalid_characters" = "Alias contains invalid characters";
"invalid_url_format" = "Please enter a valid URL (e.g., https://example.com)";
"tag_already_exists" = "Tag already exists";
/* Error Messages */
@@ -140,3 +140,9 @@
/* Basic Information */
"basic_information" = "基本信息";
"actions" = "操作";
/* Validation Messages */
"alias_already_exists" = "别名已存在";
"alias_invalid_characters" = "别名包含无效字符";
"invalid_url_format" = "请输入有效的网址(例如:https://example.com";
"tag_already_exists" = "标签已存在";
+123 -1
View File
@@ -16,7 +16,10 @@ struct LinkFormView: View {
@State private var tagSearchText: String = ""
@State private var isValidatingAlias: Bool = false
@State private var aliasValidationMessage: String = ""
@State private var urlValidationMessage: String = ""
@State private var showingTagPicker: Bool = false
@State private var saveErrorMessage: String = ""
@State private var showingSaveError: Bool = false
@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Tag.name, ascending: true)],
@@ -38,7 +41,8 @@ struct LinkFormView: View {
var canSave: Bool {
return !alias.isEmpty &&
(selectedLinkType == .custom ? !text.isEmpty : !originalURL.isEmpty) &&
aliasValidationMessage.isEmpty
aliasValidationMessage.isEmpty &&
urlValidationMessage.isEmpty
}
var body: some View {
@@ -68,6 +72,14 @@ struct LinkFormView: View {
}
}
.pickerStyle(SegmentedPickerStyle())
.onChange(of: selectedLinkType) { _, newValue in
// Re-validate URL when link type changes
if newValue != .custom {
validateURL(originalURL)
} else {
urlValidationMessage = ""
}
}
// URL or Text field based on type
if selectedLinkType == .custom {
@@ -84,6 +96,24 @@ struct LinkFormView: View {
.autocapitalization(.none)
.autocorrectionDisabled()
.keyboardType(.URL)
.textFieldStyle(RoundedBorderTextFieldStyle())
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(!urlValidationMessage.isEmpty ? Color.orange : Color.clear, lineWidth: 2)
)
.onChange(of: originalURL) { _, newValue in
validateURL(newValue)
}
if !urlValidationMessage.isEmpty {
HStack {
Image(systemName: "exclamationmark.triangle")
.foregroundColor(.orange)
Text(urlValidationMessage)
.font(.caption)
.foregroundColor(.orange)
}
}
if selectedLinkType == .template {
Text("template_help")
@@ -169,6 +199,14 @@ struct LinkFormView: View {
.onAppear {
setupForm()
}
.alert("Error", isPresented: $showingSaveError) {
Button("OK") {
showingSaveError = false
saveErrorMessage = ""
}
} message: {
Text(saveErrorMessage)
}
}
private func setupForm() {
@@ -179,6 +217,10 @@ struct LinkFormView: View {
linkDescription = link.linkDescription ?? ""
selectedLinkType = link.linkTypeEnum
selectedTags = link.sortedTags
// Validate existing data
validateAlias(alias)
validateURL(originalURL)
}
}
@@ -214,6 +256,62 @@ struct LinkFormView: View {
isValidatingAlias = false
}
private func validateURL(_ url: String) {
// Don't validate empty URLs (allow empty for templates with placeholders)
if url.isEmpty {
urlValidationMessage = ""
return
}
// For templates, allow URLs with placeholders like {query}
if selectedLinkType == .template {
// Check if it's a valid URL pattern with or without placeholders
let urlWithoutPlaceholders = url.replacingOccurrences(of: #"\{[^}]+\}"#, with: "placeholder", options: .regularExpression)
if isValidURL(urlWithoutPlaceholders) {
urlValidationMessage = ""
return
}
}
// For regular links, validate the URL strictly
if selectedLinkType == .link {
if isValidURL(url) {
urlValidationMessage = ""
return
}
}
// If we reach here, the URL is invalid
urlValidationMessage = NSLocalizedString("invalid_url_format", comment: "Invalid URL format")
}
private func isValidURL(_ urlString: String) -> Bool {
// First check if it's a valid URL
guard let url = URL(string: urlString) else {
return false
}
// Check if it has a scheme
guard let scheme = url.scheme else {
return false
}
// Allow http, https, and other common schemes
let allowedSchemes = ["http", "https", "ftp", "mailto", "tel", "sms"]
if !allowedSchemes.contains(scheme.lowercased()) {
return false
}
// Check if it has a host for http/https URLs
if ["http", "https"].contains(scheme.lowercased()) {
guard let host = url.host, !host.isEmpty else {
return false
}
}
return true
}
private func toggleTag(_ tag: Tag) {
if selectedTags.contains(where: { $0.id == tag.id }) {
selectedTags.removeAll { $0.id == tag.id }
@@ -250,6 +348,23 @@ struct LinkFormView: View {
}
private func saveLink() {
// Clear any previous error
saveErrorMessage = ""
showingSaveError = false
// Validate before saving
if !aliasValidationMessage.isEmpty {
saveErrorMessage = aliasValidationMessage
showingSaveError = true
return
}
if !urlValidationMessage.isEmpty && selectedLinkType != .custom {
saveErrorMessage = urlValidationMessage
showingSaveError = true
return
}
let success: Bool
if isEditing, let link = link {
@@ -275,6 +390,13 @@ struct LinkFormView: View {
if success {
presentationMode.wrappedValue.dismiss()
} else {
// Show error from view model
if let errorMessage = linkViewModel.errorMessage {
saveErrorMessage = errorMessage
showingSaveError = true
linkViewModel.errorMessage = nil // Clear the error
}
}
}
}