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() } /// The list's `.task`: reads only while nothing is shown yet. func loadIfNeeded() async { guard list.needsLoad else { return } await load() } func loadMilestones() async { guard milestones.isEmpty else { return } milestones = (try? await client.readList( ["milestone", "list", repoPath, "--state", "all"], of: Milestone.self )) ?? [] } /// The repository's labels for the filter picker and its chips; /// fetched on first use, like the milestones. private(set) var labels: [IssueLabel] = [] private(set) var colors = LabelColors() private var labelsLoaded = false func loadLabels() async { guard !labelsLoaded else { return } labelsLoaded = true labels = (try? await client.readList(["label", "list", repoPath], of: IssueLabel.self)) ?? [] colors = LabelColors(labels) } 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 } }