Commit 7e9802570d
Unsigned
Layout: unified · split
Hutch/App/DeepLink.swift +13 −8
| @@ -25,7 +25,7 @@ enum HutchRoute: Equatable, Sendable { | |||
| 25 | case trackers | 25 | case trackers |
| 26 | case systemStatus | 26 | case systemStatus |
| 27 | case lookup | 27 | case lookup |
| 28 | case search(query: String) | 28 | case search(query: String, type: LookupType?) |
| 29 | case projectDashboard(id: String, title: String?) | 29 | case projectDashboard(id: String, title: String?) |
| 30 | 30 | ||
| 31 | init?(url: URL) { | 31 | init?(url: URL) { |
| @@ -103,7 +103,8 @@ enum HutchRoute: Equatable, Sendable { | |||
| 103 | 103 | ||
| 104 | case "lookup": | 104 | case "lookup": |
| 105 | if let query = queryValue("q"), !query.isEmpty { | 105 | if let query = queryValue("q"), !query.isEmpty { |
| 106 | self = .search(query: query) | 106 | let type = queryValue("type").flatMap(LookupType.init(rawValue:)) |
| 107 | self = .search(query: query, type: type) | ||
| 107 | } else { | 108 | } else { |
| 108 | self = .lookup | 109 | self = .lookup |
| 109 | } | 110 | } |
| @@ -148,8 +149,12 @@ enum HutchRoute: Equatable, Sendable { | |||
| 148 | return Self.makeURL(host: "status") | 149 | return Self.makeURL(host: "status") |
| 149 | case .lookup: | 150 | case .lookup: |
| 150 | return Self.makeURL(host: "lookup") | 151 | return Self.makeURL(host: "lookup") |
| 151 | case .search(let query): | 152 | case .search(let query, let type): |
| 152 | return Self.makeURL(host: "lookup", queryItems: [URLQueryItem(name: "q", value: query)]) | 153 | var items = [URLQueryItem(name: "q", value: query)] |
| 154 | if let type { | ||
| 155 | items.append(URLQueryItem(name: "type", value: type.rawValue)) | ||
| 156 | } | ||
| 157 | return Self.makeURL(host: "lookup", queryItems: items) | ||
| 153 | case .projectDashboard(let id, let title): | 158 | case .projectDashboard(let id, let title): |
| 154 | return Self.makeURL( | 159 | return Self.makeURL( |
| 155 | host: "projects", | 160 | host: "projects", |
| @@ -205,8 +210,8 @@ enum DeepLink: Equatable { | |||
| 205 | case systemStatus | 210 | case systemStatus |
| 206 | /// hutch://lookup | 211 | /// hutch://lookup |
| 207 | case lookup | 212 | case lookup |
| 208 | /// hutch://lookup?q=<query> | 213 | /// hutch://lookup?q=<query>&type=<type> |
| 209 | case search(query: String) | 214 | case search(query: String, type: LookupType?) |
| 210 | /// hutch://builds?filter=failed | 215 | /// hutch://builds?filter=failed |
| 211 | case failedBuilds | 216 | case failedBuilds |
| 212 | /// hutch://projects/<rid> | 217 | /// hutch://projects/<rid> |
| @@ -253,8 +258,8 @@ enum DeepLink: Equatable { | |||
| 253 | self = .systemStatus | 258 | self = .systemStatus |
| 254 | case .lookup: | 259 | case .lookup: |
| 255 | self = .lookup | 260 | self = .lookup |
| 256 | case .search(let query): | 261 | case .search(let query, let type): |
| 257 | self = .search(query: query) | 262 | self = .search(query: query, type: type) |
| 258 | case .projectDashboard(let id, let title): | 263 | case .projectDashboard(let id, let title): |
| 259 | self = .projectDashboard(id: id, title: title) | 264 | self = .projectDashboard(id: id, title: title) |
| 260 | } | 265 | } |
Hutch/App/HutchIntents.swift +104 −43
| @@ -132,11 +132,14 @@ struct SearchHutchIntent: AppIntent { | |||
| 132 | @Parameter(title: "Query") | 132 | @Parameter(title: "Query") |
| 133 | var query: String | 133 | var query: String |
| 134 | 134 | ||
| 135 | @Parameter(title: "Search Type", default: .user) | ||
| 136 | var searchType: LookupType | ||
| 137 | |||
| 135 | var route: HutchRoute { | 138 | var route: HutchRoute { |
| 136 | let normalized = query.trimmingCharacters(in: .whitespacesAndNewlines) | 139 | let normalized = query.trimmingCharacters(in: .whitespacesAndNewlines) |
| 137 | // Routes to Lookup for now; repoint at a global content search when Hutch | 140 | // Routes to Lookup, pre-selecting the search type, until Hutch gains a |
| 138 | // gains one — tracked in ROADMAP.md § "App Intent gaps". | 141 | // global content search. |
| 139 | return normalized.isEmpty ? .lookup : .search(query: normalized) | 142 | return normalized.isEmpty ? .lookup : .search(query: normalized, type: searchType) |
| 140 | } | 143 | } |
| 141 | 144 | ||
| 142 | @MainActor | 145 | @MainActor |
| @@ -147,7 +150,7 @@ struct SearchHutchIntent: AppIntent { | |||
| 147 | } | 150 | } |
| 148 | 151 | ||
| 149 | // An OpenSavedSearchIntent belongs here once Hutch has global saved-search | 152 | // An OpenSavedSearchIntent belongs here once Hutch has global saved-search |
| 150 | // persistence — tracked in ROADMAP.md § "App Intent gaps". | 153 | // persistence. |
| 151 | 154 | ||
| 152 | // MARK: - App Entities | 155 | // MARK: - App Entities |
| 153 | 156 | ||
| @@ -201,7 +204,7 @@ struct ProjectEntityQuery: EntityQuery { | |||
| 201 | } | 204 | } |
| 202 | } | 205 | } |
| 203 | 206 | ||
| 204 | private enum HutchIntentEntityStore { | 207 | enum HutchIntentEntityStore { |
| 205 | static func pinnedResources() -> [PinnedResourceEntity] { | 208 | static func pinnedResources() -> [PinnedResourceEntity] { |
| 206 | pins().compactMap { makePinnedResource(from: $0) } | 209 | pins().compactMap { makePinnedResource(from: $0) } |
| 207 | } | 210 | } |
| @@ -213,22 +216,27 @@ private enum HutchIntentEntityStore { | |||
| 213 | } | 216 | } |
| 214 | } | 217 | } |
| 215 | 218 | ||
| 216 | private static func pins() -> [HomePinRecord] { | 219 | /// The active account's key, or `nil` when no account is signed in. |
| 220 | static func currentUserKey() -> String? { | ||
| 217 | guard let userKey = ContributionWidgetContextStore.loadActor(), | 221 | guard let userKey = ContributionWidgetContextStore.loadActor(), |
| 218 | !userKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty | 222 | !userKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty |
| 219 | else { | 223 | else { |
| 220 | return [] | 224 | return nil |
| 221 | } | 225 | } |
| 222 | 226 | return userKey | |
| 223 | return HomePinStore.loadPins(for: userKey, defaults: activeAccountDefaults) | ||
| 224 | } | 227 | } |
| 225 | 228 | ||
| 226 | private static var activeAccountDefaults: UserDefaults { | 229 | static var accountDefaults: UserDefaults { |
| 227 | let activeID = UserDefaults.standard.string(forKey: AppStorageKeys.activeAccountID) ?? "" | 230 | let activeID = UserDefaults.standard.string(forKey: AppStorageKeys.activeAccountID) ?? "" |
| 228 | guard !activeID.isEmpty else { return .standard } | 231 | guard !activeID.isEmpty else { return .standard } |
| 229 | return AccountDefaultsStore.userDefaults(for: activeID) | 232 | return AccountDefaultsStore.userDefaults(for: activeID) |
| 230 | } | 233 | } |
| 231 | 234 | ||
| 235 | private static func pins() -> [HomePinRecord] { | ||
| 236 | guard let userKey = currentUserKey() else { return [] } | ||
| 237 | return HomePinStore.loadPins(for: userKey, defaults: accountDefaults) | ||
| 238 | } | ||
| 239 | |||
| 232 | private static func makePinnedResource(from pin: HomePinRecord) -> PinnedResourceEntity? { | 240 | private static func makePinnedResource(from pin: HomePinRecord) -> PinnedResourceEntity? { |
| 233 | guard let route = route(for: pin) else { return nil } | 241 | guard let route = route(for: pin) else { return nil } |
| 234 | return PinnedResourceEntity( | 242 | return PinnedResourceEntity( |
| @@ -274,20 +282,22 @@ struct CheckSystemStatusIntent: AppIntent { | |||
| 274 | static var description = IntentDescription("Returns the current SourceHut system status.") | 282 | static var description = IntentDescription("Returns the current SourceHut system status.") |
| 275 | 283 | ||
| 276 | @MainActor | 284 | @MainActor |
| 277 | func perform() async throws -> some IntentResult & ReturnsValue<String> { | 285 | func perform() async throws -> some IntentResult & ReturnsValue<String> & ProvidesDialog { |
| 278 | guard let snapshot = SystemStatusWidgetSnapshotStore.load() else { | 286 | let message: String |
| 279 | return .result(value: "System status is unavailable. Open Hutch to refresh.") | 287 | if let snapshot = SystemStatusWidgetSnapshotStore.load() { |
| 280 | } | 288 | if snapshot.hasDisruption { |
| 281 | 289 | let disrupted = snapshot.services | |
| 282 | if snapshot.hasDisruption { | 290 | .filter { $0.requiresAttention } |
| 283 | let disrupted = snapshot.services | 291 | .map { "\($0.name): \($0.status)" } |
| 284 | .filter { $0.requiresAttention } | 292 | .joined(separator: ", ") |
| 285 | .map { "\($0.name): \($0.status)" } | 293 | message = "SourceHut disruption detected: \(disrupted)" |
| 286 | .joined(separator: ", ") | 294 | } else { |
| 287 | return .result(value: "SourceHut disruption detected: \(disrupted)") | 295 | message = "All SourceHut services operational." |
| 296 | } | ||
| 297 | } else { | ||
| 298 | message = "System status is unavailable. Open Hutch to refresh." | ||
| 288 | } | 299 | } |
| 289 | 300 | return .result(value: message, dialog: IntentDialog(stringLiteral: message)) | |
| 290 | return .result(value: "All SourceHut services operational.") | ||
| 291 | } | 301 | } |
| 292 | } | 302 | } |
| 293 | 303 | ||
| @@ -296,34 +306,85 @@ struct CheckBuildsIntent: AppIntent { | |||
| 296 | static var description = IntentDescription("Returns a summary of your recent build status.") | 306 | static var description = IntentDescription("Returns a summary of your recent build status.") |
| 297 | 307 | ||
| 298 | @MainActor | 308 | @MainActor |
| 299 | func perform() async throws -> some IntentResult & ReturnsValue<String> { | 309 | func perform() async throws -> some IntentResult & ReturnsValue<String> & ProvidesDialog { |
| 300 | guard let snapshot = NeedsAttentionSnapshotStore.load() else { | 310 | let message: String |
| 301 | return .result(value: "Build status unavailable. Open Hutch to refresh.") | 311 | if let snapshot = NeedsAttentionSnapshotStore.load() { |
| 302 | } | 312 | var parts: [String] = [] |
| 313 | |||
| 314 | if let failed = snapshot.failedBuilds { | ||
| 315 | if failed > 0 { | ||
| 316 | parts.append("\(failed) failed build\(failed == 1 ? "" : "s")") | ||
| 317 | } else { | ||
| 318 | parts.append("No failed builds") | ||
| 319 | } | ||
| 320 | } | ||
| 303 | 321 | ||
| 304 | var parts: [String] = [] | 322 | if let unread = snapshot.unreadInboxThreads, unread > 0 { |
| 323 | parts.append("\(unread) unread thread\(unread == 1 ? "" : "s")") | ||
| 324 | } | ||
| 305 | 325 | ||
| 306 | if let failed = snapshot.failedBuilds { | 326 | if let assigned = snapshot.assignedOpenTickets, assigned > 0 { |
| 307 | if failed > 0 { | 327 | parts.append("\(assigned) assigned ticket\(assigned == 1 ? "" : "s")") |
| 308 | parts.append("\(failed) failed build\(failed == 1 ? "" : "s")") | ||
| 309 | } else { | ||
| 310 | parts.append("No failed builds") | ||
| 311 | } | 328 | } |
| 312 | } | ||
| 313 | 329 | ||
| 314 | if let unread = snapshot.unreadInboxThreads, unread > 0 { | 330 | message = parts.isEmpty ? "No recent data. Open Hutch to refresh." : parts.joined(separator: ". ") + "." |
| 315 | parts.append("\(unread) unread thread\(unread == 1 ? "" : "s")") | 331 | } else { |
| 332 | message = "Build status unavailable. Open Hutch to refresh." | ||
| 316 | } | 333 | } |
| 334 | return .result(value: message, dialog: IntentDialog(stringLiteral: message)) | ||
| 335 | } | ||
| 336 | } | ||
| 317 | 337 | ||
| 318 | if let assigned = snapshot.assignedOpenTickets, assigned > 0 { | 338 | // MARK: - Search Type |
| 319 | parts.append("\(assigned) assigned ticket\(assigned == 1 ? "" : "s")") | ||
| 320 | } | ||
| 321 | 339 | ||
| 322 | if parts.isEmpty { | 340 | extension LookupType: @retroactive AppEnum { |
| 323 | return .result(value: "No recent data. Open Hutch to refresh.") | 341 | public nonisolated static var typeDisplayRepresentation: TypeDisplayRepresentation { |
| 324 | } | 342 | TypeDisplayRepresentation(name: "Search Type") |
| 343 | } | ||
| 344 | |||
| 345 | public nonisolated static var caseDisplayRepresentations: [LookupType: DisplayRepresentation] { | ||
| 346 | [ | ||
| 347 | .user: "User", | ||
| 348 | .gitRepo: "Git Repository", | ||
| 349 | .hgRepo: "Mercurial Repository", | ||
| 350 | .mailingList: "Mailing List", | ||
| 351 | .tracker: "Tracker", | ||
| 352 | .buildJob: "Build Job" | ||
| 353 | ] | ||
| 354 | } | ||
| 355 | } | ||
| 356 | |||
| 357 | // MARK: - Mutating Intents | ||
| 358 | |||
| 359 | struct ClearRecentActivityIntent: AppIntent { | ||
| 360 | static var title: LocalizedStringResource = "Clear Recent Activity" | ||
| 361 | static var description = IntentDescription("Clears the Recent list on the Hutch Home tab.") | ||
| 362 | |||
| 363 | @MainActor | ||
| 364 | func perform() async throws -> some IntentResult & ProvidesDialog { | ||
| 365 | RecentActivityStore.clear(defaults: HutchIntentEntityStore.accountDefaults) | ||
| 366 | return .result(dialog: "Cleared recent activity.") | ||
| 367 | } | ||
| 368 | } | ||
| 369 | |||
| 370 | struct UnpinResourceIntent: AppIntent { | ||
| 371 | static var title: LocalizedStringResource = "Unpin Resource" | ||
| 372 | static var description = IntentDescription("Removes a pinned resource from the Hutch Home tab.") | ||
| 325 | 373 | ||
| 326 | return .result(value: parts.joined(separator: ". ") + ".") | 374 | @Parameter(title: "Pinned Resource") |
| 375 | var pinnedResource: PinnedResourceEntity | ||
| 376 | |||
| 377 | @MainActor | ||
| 378 | func perform() async throws -> some IntentResult & ProvidesDialog { | ||
| 379 | guard let userKey = HutchIntentEntityStore.currentUserKey() else { | ||
| 380 | return .result(dialog: "No active Hutch account.") | ||
| 381 | } | ||
| 382 | HomePinStore.removePin( | ||
| 383 | id: pinnedResource.id, | ||
| 384 | for: userKey, | ||
| 385 | defaults: HutchIntentEntityStore.accountDefaults | ||
| 386 | ) | ||
| 387 | return .result(dialog: "Unpinned \(pinnedResource.name).") | ||
| 327 | } | 388 | } |
| 328 | } | 389 | } |
| 329 | 390 | ||
Hutch/App/RootView.swift +6 −6
| @@ -247,12 +247,12 @@ struct RootView: View { | |||
| 247 | appState.pendingBuildListFilter = .failed | 247 | appState.pendingBuildListFilter = .failed |
| 248 | appState.selectedTab = .builds | 248 | appState.selectedTab = .builds |
| 249 | 249 | ||
| 250 | case .search(let query): | 250 | case .search(let query, let type): |
| 251 | morePath = NavigationPath() | 251 | morePath = NavigationPath() |
| 252 | appState.selectedTab = .more | 252 | appState.selectedTab = .more |
| 253 | Task { | 253 | Task { |
| 254 | await settleNavigationTransition() | 254 | await settleNavigationTransition() |
| 255 | morePath.append(MoreRoute.lookup(query: query)) | 255 | morePath.append(MoreRoute.lookup(query: query, type: type)) |
| 256 | } | 256 | } |
| 257 | 257 | ||
| 258 | case .lookup: | 258 | case .lookup: |
| @@ -260,7 +260,7 @@ struct RootView: View { | |||
| 260 | appState.selectedTab = .more | 260 | appState.selectedTab = .more |
| 261 | Task { | 261 | Task { |
| 262 | await settleNavigationTransition() | 262 | await settleNavigationTransition() |
| 263 | morePath.append(MoreRoute.lookup(query: nil)) | 263 | morePath.append(MoreRoute.lookup(query: nil, type: nil)) |
| 264 | } | 264 | } |
| 265 | 265 | ||
| 266 | case .buildsTab: | 266 | case .buildsTab: |
| @@ -431,7 +431,7 @@ enum MoreDestination: Hashable { | |||
| 431 | } | 431 | } |
| 432 | 432 | ||
| 433 | enum MoreRoute: Hashable { | 433 | enum MoreRoute: Hashable { |
| 434 | case lookup(query: String?) | 434 | case lookup(query: String?, type: LookupType?) |
| 435 | case projects | 435 | case projects |
| 436 | case lists | 436 | case lists |
| 437 | case pastes | 437 | case pastes |
| @@ -454,8 +454,8 @@ private struct MoreNavigationRoot: View { | |||
| 454 | MoreView() | 454 | MoreView() |
| 455 | .navigationDestination(for: MoreRoute.self) { route in | 455 | .navigationDestination(for: MoreRoute.self) { route in |
| 456 | switch route { | 456 | switch route { |
| 457 | case .lookup(let query): | 457 | case .lookup(let query, let type): |
| 458 | LookupView(initialQuery: query ?? "") | 458 | LookupView(initialQuery: query ?? "", initialType: type) |
| 459 | case .projects: | 459 | case .projects: |
| 460 | ProjectsListView() | 460 | ProjectsListView() |
| 461 | case .lists: | 461 | case .lists: |
Hutch/Views/Home/HomePinStore.swift +15
| @@ -144,6 +144,21 @@ enum HomePinStore { | |||
| 144 | saveAll(pinsByUser, defaults: defaults) | 144 | saveAll(pinsByUser, defaults: defaults) |
| 145 | } | 145 | } |
| 146 | 146 | ||
| 147 | static func removePin( | ||
| 148 | id: String, | ||
| 149 | for userKey: String, | ||
| 150 | defaults: UserDefaults = .standard | ||
| 151 | ) { | ||
| 152 | let normalizedUserKey = normalizedUserKey(userKey) | ||
| 153 | guard !normalizedUserKey.isEmpty else { return } | ||
| 154 | |||
| 155 | var pinsByUser = loadAll(defaults: defaults) | ||
| 156 | var pins = normalizedPins(pinsByUser[normalizedUserKey] ?? loadPins(for: normalizedUserKey, defaults: defaults)) | ||
| 157 | pins.removeAll { $0.id == id } | ||
| 158 | pinsByUser[normalizedUserKey] = pins | ||
| 159 | saveAll(pinsByUser, defaults: defaults) | ||
| 160 | } | ||
| 161 | |||
| 147 | static func pinnedProjectIDs( | 162 | static func pinnedProjectIDs( |
| 148 | for userKey: String, | 163 | for userKey: String, |
| 149 | defaults: UserDefaults = .standard | 164 | defaults: UserDefaults = .standard |
Hutch/Views/Lookup/LookupView.swift +12 −5
| @@ -96,13 +96,17 @@ final class LookupViewModel { | |||
| 96 | client: SRHTClient, | 96 | client: SRHTClient, |
| 97 | appState: AppState, | 97 | appState: AppState, |
| 98 | defaults: UserDefaults = .standard, | 98 | defaults: UserDefaults = .standard, |
| 99 | initialQuery: String = "" | 99 | initialQuery: String = "", |
| 100 | initialType: LookupType? = nil | ||
| 100 | ) { | 101 | ) { |
| 101 | self.client = client | 102 | self.client = client |
| 102 | self.appState = appState | 103 | self.appState = appState |
| 103 | self.defaults = defaults | 104 | self.defaults = defaults |
| 104 | self.history = LookupHistoryStore.load(defaults: defaults) | 105 | self.history = LookupHistoryStore.load(defaults: defaults) |
| 105 | self.inputText = initialQuery.trimmingCharacters(in: .whitespacesAndNewlines) | 106 | self.inputText = initialQuery.trimmingCharacters(in: .whitespacesAndNewlines) |
| 107 | if let initialType { | ||
| 108 | self.selectedType = initialType | ||
| 109 | } | ||
| 106 | } | 110 | } |
| 107 | 111 | ||
| 108 | func lookup() async { | 112 | func lookup() async { |
| @@ -336,9 +340,11 @@ struct LookupView: View { | |||
| 336 | @Environment(AppState.self) private var appState | 340 | @Environment(AppState.self) private var appState |
| 337 | @State private var viewModel: LookupViewModel? | 341 | @State private var viewModel: LookupViewModel? |
| 338 | private let initialQuery: String | 342 | private let initialQuery: String |
| 343 | private let initialType: LookupType? | ||
| 339 | 344 | ||
| 340 | init(initialQuery: String = "") { | 345 | init(initialQuery: String = "", initialType: LookupType? = nil) { |
| 341 | self.initialQuery = initialQuery | 346 | self.initialQuery = initialQuery |
| 347 | self.initialType = initialType | ||
| 342 | } | 348 | } |
| 343 | 349 | ||
| 344 | var body: some View { | 350 | var body: some View { |
| @@ -356,7 +362,8 @@ struct LookupView: View { | |||
| 356 | client: appState.client, | 362 | client: appState.client, |
| 357 | appState: appState, | 363 | appState: appState, |
| 358 | defaults: appState.accountDefaults, | 364 | defaults: appState.accountDefaults, |
| 359 | initialQuery: initialQuery | 365 | initialQuery: initialQuery, |
| 366 | initialType: initialType | ||
| 360 | ) | 367 | ) |
| 361 | } | 368 | } |
| 362 | } | 369 | } |
| @@ -443,8 +450,8 @@ struct LookupView: View { | |||
| 443 | } | 450 | } |
| 444 | .navigationDestination(for: MoreRoute.self) { route in | 451 | .navigationDestination(for: MoreRoute.self) { route in |
| 445 | switch route { | 452 | switch route { |
| 446 | case .lookup(let query): | 453 | case .lookup(let query, let type): |
| 447 | LookupView(initialQuery: query ?? "") | 454 | LookupView(initialQuery: query ?? "", initialType: type) |
| 448 | case .projects: | 455 | case .projects: |
| 449 | ProjectsListView() | 456 | ProjectsListView() |
| 450 | case .lists: | 457 | case .lists: |
Hutch/Views/More/MoreView.swift +1 −1
| @@ -13,7 +13,7 @@ struct MoreView: View { | |||
| 13 | var body: some View { | 13 | var body: some View { |
| 14 | List { | 14 | List { |
| 15 | Section("Search") { | 15 | Section("Search") { |
| 16 | NavigationLink(value: MoreRoute.lookup(query: nil)) { | 16 | NavigationLink(value: MoreRoute.lookup(query: nil, type: nil)) { |
| 17 | Label("Look Up", systemImage: "magnifyingglass") | 17 | Label("Look Up", systemImage: "magnifyingglass") |
| 18 | } | 18 | } |
| 19 | .themedRow() | 19 | .themedRow() |
HutchTests/DeepLinkTests.swift +2 −1
| @@ -92,7 +92,8 @@ struct DeepLinkTests { | |||
| 92 | func parsesRouteBackedNavigationLinks() { | 92 | func parsesRouteBackedNavigationLinks() { |
| 93 | #expect(DeepLink(url: HutchRoute.workQueue(scope: .assigned).url) == .workQueue(scope: .assigned)) | 93 | #expect(DeepLink(url: HutchRoute.workQueue(scope: .assigned).url) == .workQueue(scope: .assigned)) |
| 94 | #expect(DeepLink(url: HutchRoute.failedBuilds.url) == .failedBuilds) | 94 | #expect(DeepLink(url: HutchRoute.failedBuilds.url) == .failedBuilds) |
| 95 | #expect(DeepLink(url: HutchRoute.search(query: "patch queue").url) == .search(query: "patch queue")) | 95 | #expect(DeepLink(url: HutchRoute.search(query: "patch queue", type: nil).url) == .search(query: "patch queue", type: nil)) |
| 96 | #expect(DeepLink(url: HutchRoute.search(query: "~alice", type: .user).url) == .search(query: "~alice", type: .user)) | ||
| 96 | #expect(DeepLink(url: HutchRoute.projectDashboard(id: "project-1", title: "Hutch").url) == .projectDashboard(id: "project-1", title: "Hutch")) | 97 | #expect(DeepLink(url: HutchRoute.projectDashboard(id: "project-1", title: "Hutch").url) == .projectDashboard(id: "project-1", title: "Hutch")) |
| 97 | } | 98 | } |
| 98 | 99 | ||
HutchTests/HutchIntentsTests.swift +33
| @@ -110,4 +110,37 @@ struct HutchIntentsTests { | |||
| 110 | ActiveAccountContextStore.save("account-a", defaults: defaults) | 110 | ActiveAccountContextStore.save("account-a", defaults: defaults) |
| 111 | #expect(NeedsAttentionSnapshotStore.load(defaults: defaults)?.failedBuilds == 3) | 111 | #expect(NeedsAttentionSnapshotStore.load(defaults: defaults)?.failedBuilds == 3) |
| 112 | } | 112 | } |
| 113 | |||
| 114 | @Test | ||
| 115 | func searchIntentCarriesQueryAndType() { | ||
| 116 | let intent = SearchHutchIntent() | ||
| 117 | intent.query = "~alice/hutch" | ||
| 118 | intent.searchType = .tracker | ||
| 119 | #expect(intent.route == .search(query: "~alice/hutch", type: .tracker)) | ||
| 120 | } | ||
| 121 | |||
| 122 | @Test | ||
| 123 | func searchIntentWithBlankQueryFallsBackToLookup() { | ||
| 124 | let intent = SearchHutchIntent() | ||
| 125 | intent.query = " " | ||
| 126 | intent.searchType = .user | ||
| 127 | #expect(intent.route == .lookup) | ||
| 128 | } | ||
| 129 | |||
| 130 | @Test | ||
| 131 | func removePinDropsMatchingResource() { | ||
| 132 | let defaultsName = "HutchIntentsTests-unpin-\(UUID().uuidString)" | ||
| 133 | let defaults = UserDefaults(suiteName: defaultsName)! | ||
| 134 | defer { defaults.removePersistentDomain(forName: defaultsName) } | ||
| 135 | |||
| 136 | let alice = HomePinRecord(kind: .user, value: "~alice", title: "~alice", subtitle: "User", ownerUsername: "~alice", service: nil) | ||
| 137 | let bob = HomePinRecord(kind: .user, value: "~bob", title: "~bob", subtitle: "User", ownerUsername: "~bob", service: nil) | ||
| 138 | HomePinStore.togglePin(alice, for: "~me", defaults: defaults) | ||
| 139 | HomePinStore.togglePin(bob, for: "~me", defaults: defaults) | ||
| 140 | |||
| 141 | HomePinStore.removePin(id: alice.id, for: "~me", defaults: defaults) | ||
| 142 | |||
| 143 | let remaining = HomePinStore.loadPins(for: "~me", defaults: defaults) | ||
| 144 | #expect(remaining.map(\.id) == [bob.id]) | ||
| 145 | } | ||
| 113 | } | 146 | } |