import Foundation import Observation /// `mr list --state `, paginated. @Observable @MainActor final class MRListViewModel { let list: PagedListModel let repoPath: String private let client: GitbayClient var filter = MRFilter() { didSet { guard filter != oldValue else { return } configureList() // Only the free-text fields changed: debounce, so typing does // not fire a request per keystroke. Anything else — a picker — // reloads now. MRFilter has no `assignee` field: `mr list` // rejects it. var typed = oldValue typed.search = filter.search typed.author = filter.author if typed == filter { scheduleSearchReload() } else { reloadTask?.cancel() reloadTask = Task { await list.reload() } } } } /// Open and closed milestones for the filter picker; fetched on first use. private(set) var milestones: [Milestone] = [] private var reloadTask: Task? init(client: GitbayClient, repoPath: String) { self.client = client self.repoPath = repoPath list = PagedListModel( client: client, argv: ["mr", "list", repoPath] + MRFilter().flags(), emptyMessage: MRFilter().emptyMessage ) } var state: LoadState<[MergeRequest]> { list.state } func load() async { reloadTask?.cancel() await list.reload() } func loadMilestones() async { guard milestones.isEmpty else { return } milestones = (try? await client.readList( ["milestone", "list", repoPath, "--state", "all"], of: Milestone.self )) ?? [] } private func scheduleSearchReload() { reloadTask?.cancel() reloadTask = Task { guard (try? await Task.sleep(for: .milliseconds(300))) != nil else { return } guard !Task.isCancelled else { return } await list.reload() } } private func configureList() { list.argv = ["mr", "list", repoPath] + filter.flags() list.emptyMessage = filter.emptyMessage } }