From 0442c2bb687d8b6bebe08d3db2482696a4cc5d4a Mon Sep 17 00:00:00 2001 From: Junwei Zhao Date: Sat, 16 Aug 2025 20:24:00 +1000 Subject: [PATCH] Fix import link error --- mobile/Heygo/Views/SettingsView.swift | 99 +++++++++++++++++---------- 1 file changed, 64 insertions(+), 35 deletions(-) diff --git a/mobile/Heygo/Views/SettingsView.swift b/mobile/Heygo/Views/SettingsView.swift index b8add70..dd271f3 100644 --- a/mobile/Heygo/Views/SettingsView.swift +++ b/mobile/Heygo/Views/SettingsView.swift @@ -81,6 +81,49 @@ struct SettingsView: View { var skippedCount = 0 var failedEntries: [String] = [] + // Helper to generate slug similar to backend slugify + func generateSlug(_ raw: String) -> String { + let base = raw + .lowercased() + .trimmingCharacters(in: .whitespacesAndNewlines) + .replacingOccurrences(of: " ", with: "-") + .replacingOccurrences(of: "[^a-z0-9-]", with: "", options: .regularExpression) + return base + } + + // Fetch or create tag ensuring unique slug + func fetchOrCreateTag(named name: String) throws -> Tag { + let trimmed = name.trimmingCharacters(in: .whitespacesAndNewlines) + if trimmed.isEmpty { throw NSError(domain: "Import", code: 10, userInfo: [NSLocalizedDescriptionKey: "Empty tag name"]) } + + // Case-insensitive fetch + let request: NSFetchRequest = Tag.fetchRequest() + request.predicate = NSPredicate(format: "name ==[c] %@", trimmed) + request.fetchLimit = 1 + if let existing = try context.fetch(request).first { return existing } + + // Create new tag with unique slug + let tag = Tag(context: context) + tag.name = trimmed + var slug = generateSlug(trimmed) + if slug.isEmpty { slug = UUID().uuidString.prefix(8).lowercased() } + + // Ensure slug uniqueness + var uniqueSlug = slug + var index = 1 + while true { + let slugRequest: NSFetchRequest = Tag.fetchRequest() + slugRequest.predicate = NSPredicate(format: "slug == %@", uniqueSlug) + slugRequest.fetchLimit = 1 + if try context.count(for: slugRequest) == 0 { break } + uniqueSlug = "\(slug)-\(index)" + index += 1 + } + tag.slug = uniqueSlug + tag.createdAt = Date() + return tag + } + // First try to parse the entire JSON as LinkData array do { let linkDataArray = try JSONDecoder().decode([LinkData].self, from: data) @@ -106,24 +149,12 @@ struct SettingsView: View { // Handle tags if present if let tagNames = linkData.tags { for tagName in tagNames { - let tagFetchRequest: NSFetchRequest = Tag.fetchRequest() - tagFetchRequest.predicate = NSPredicate(format: "name == %@", tagName) - - var tag: Tag - if let existingTag = try context.fetch(tagFetchRequest).first { - tag = existingTag - } else { - tag = Tag(context: context) - tag.name = tagName - // Generate slug (required non-optional Core Data attribute) - tag.slug = tagName - .lowercased() - .trimmingCharacters(in: .whitespacesAndNewlines) - .replacingOccurrences(of: " ", with: "-") - .replacingOccurrences(of: "[^a-z0-9-]", with: "", options: .regularExpression) - tag.createdAt = Date() + do { + let tag = try fetchOrCreateTag(named: tagName) + link.addToTags(tag) + } catch { + print("⚠️ Skipped tag '\(tagName)': \(error.localizedDescription)") } - link.addToTags(tag) } } @@ -170,24 +201,12 @@ struct SettingsView: View { // Handle tags if present if let tagNames = linkData.tags { for tagName in tagNames { - let tagFetchRequest: NSFetchRequest = Tag.fetchRequest() - tagFetchRequest.predicate = NSPredicate(format: "name == %@", tagName) - - var tag: Tag - if let existingTag = try context.fetch(tagFetchRequest).first { - tag = existingTag - } else { - tag = Tag(context: context) - tag.name = tagName - // Generate slug (required non-optional Core Data attribute) - tag.slug = tagName - .lowercased() - .trimmingCharacters(in: .whitespacesAndNewlines) - .replacingOccurrences(of: " ", with: "-") - .replacingOccurrences(of: "[^a-z0-9-]", with: "", options: .regularExpression) - tag.createdAt = Date() + do { + let tag = try fetchOrCreateTag(named: tagName) + link.addToTags(tag) + } catch { + print("⚠️ Skipped tag '\(tagName)': \(error.localizedDescription)") } - link.addToTags(tag) } } @@ -204,7 +223,17 @@ struct SettingsView: View { } } - try context.save() + do { + try context.save() + } catch { + let nsError = error as NSError + if let detailed = nsError.userInfo[NSDetailedErrorsKey] as? [NSError] { + for d in detailed { print("❌ Save detail: \(d), userInfo: \(d.userInfo)") } + } else { + print("❌ Save error: \(nsError), userInfo: \(nsError.userInfo)") + } + throw nsError + } // Refresh the link view model linkViewModel.fetchLinks()