import Foundation import Observation /// `issue list --state `, paginated. @Observable @MainActor final class IssueListViewModel { let list: PagedListModel let repoPath: String private let client: GitbayClient var filter = IssueFilter() { 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. var typed = oldValue typed.search = filter.search typed.author = filter.author typed.assignee = filter.assignee if typed == filter { scheduleSearchReload() } else { reloadTask?.cancel() reloadTask = Task { await list.reload() } } } } /// This repository's labels, loaded beside the issues: the names for /// a picker, the colours for the chips. A failure here is not fatal — /// every chip then derives its colour from its name. private(set) var labels: [IssueLabel] = [] private(set) var colors = LabelColors() /// Labels rarely change mid-session: fetch them once per view model, /// not on every reload. private var labelsLoaded = false /// 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: ["issue", "list", repoPath] + IssueFilter().flags(), emptyMessage: IssueFilter().emptyMessage ) } var state: LoadState<[Issue]> { list.state } func load() async { reloadTask?.cancel() await list.reload() if case .failed = list.state { return } guard !labelsLoaded else { return } labelsLoaded = true labels = await labelList() colors = LabelColors(labels) } func loadMilestones() async { guard milestones.isEmpty else { return } milestones = (try? await client.readList( ["milestone", "list", repoPath, "--state", "all"], of: Milestone.self )) ?? [] } private func labelList() async -> [IssueLabel] { (try? await client.readList(["label", "list", repoPath], of: IssueLabel.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 = ["issue", "list", repoPath] + filter.flags() list.emptyMessage = filter.emptyMessage } }