import SwiftUI struct IssueListView: View { @State private var model: IssueListViewModel @State private var createModel: IssueCreateViewModel @State private var composing = false @State private var filtering = false @State private var draftTitle = "" @State private var draftBody = "" @State private var draftFormat = "md" @Environment(\.pushRoute) private var pushRoute private let client: GitbayClient init(client: GitbayClient, repo: String) { self.client = client _model = State(initialValue: IssueListViewModel(client: client, repoPath: repo)) _createModel = State(initialValue: IssueCreateViewModel(client: client, repoPath: repo)) } var body: some View { List { Section { Picker("State", selection: Bindable(model).filter.state) { ForEach(IssueFilter.State.allCases) { filter in Text(filter.label).tag(filter) } } .pickerStyle(.segmented) .listRowBackground(Color.clear) .listRowInsets(EdgeInsets()) } ForEach(model.state.value ?? []) { issue in NavigationLink(value: IssueRoute.issue(repo: model.repoPath, number: issue.number)) { IssueRow(issue: issue, colors: model.colors) } } PageFooter(list: model.list) } .overlay { LoadStateOverlay(state: model.state) } .searchable(text: Bindable(model).filter.search, prompt: "Search titles and bodies") .navigationTitle("Issues") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .topBarTrailing) { Button { filtering = true } label: { Image(systemName: model.filter.isNarrowed ? "line.3.horizontal.decrease.circle.fill" : "line.3.horizontal.decrease.circle") } .accessibilityLabel("Filter") .accessibilityIdentifier("issue-filter-button") } ToolbarItem(placement: .topBarTrailing) { Button { composing = true } label: { Image(systemName: "plus") } .accessibilityLabel("New Issue") .accessibilityIdentifier("issue-create-button") } } .sheet(isPresented: $filtering) { FilterSheet( title: "Filter issues", author: Bindable(model).filter.author, milestone: Bindable(model).filter.milestone, milestones: model.milestones, onClear: { let state = model.filter.state model.filter = IssueFilter(state: state) } ) { IssueFilterExtra( label: Bindable(model).filter.label, assignee: Bindable(model).filter.assignee, labels: model.labels, colors: model.colors ) } .task { await model.loadMilestones() } } .sheet(isPresented: $composing) { ComposeSheet( heading: "New Issue", submitLabel: "Create", working: createModel.working, errorMessage: createModel.errorMessage, title: $draftTitle, bodyText: $draftBody, format: $draftFormat, templates: createModel.templates ) { createModel.format = draftFormat Task { if let number = await createModel.create(title: draftTitle, body: draftBody) { draftTitle = "" draftBody = "" draftFormat = "md" composing = false pushRoute(IssueRoute.issue(repo: model.repoPath, number: number)) await model.load() } } } } .task { await model.loadIfNeeded() } .task { await createModel.loadTemplates() } .refreshable { await model.load() } } } private struct IssueRow: View { let issue: Issue let colors: LabelColors @Environment(\.dynamicTypeSize) private var dynamicTypeSize var body: some View { let large = dynamicTypeSize.isAccessibilitySize let layout = large ? AnyLayout(VStackLayout(alignment: .leading, spacing: 4)) : AnyLayout(HStackLayout(spacing: 6)) VStack(alignment: .leading, spacing: 4) { HStack(alignment: .firstTextBaseline, spacing: 6) { Text("#\(issue.number)") .font(.gbMono(.caption)) .foregroundStyle(.secondary) Text(issue.title) .font(.gbSans(.subheadline).weight(.medium)) .lineLimit(2) } layout { HStack(spacing: 6) { Image(systemName: issue.isOpen ? "circle" : "checkmark.circle.fill") .font(.gbSans(.caption2)) .foregroundStyle(issue.isOpen ? Color.gbOK : Color.gbBad) if large { ViewThatFits(in: .horizontal) { HStack(spacing: 6) { labels } VStack(alignment: .leading, spacing: 4) { labels } } } else { labels } } if !large { Spacer() } if let assignees = issue.assignees, !assignees.isEmpty { Text(assignees.joined(separator: ", ")) .font(.gbSans(.caption)) .foregroundStyle(.secondary) .lineLimit(1) } Text(issue.createdAt, format: .relative(presentation: .named)) .font(.gbSans(.caption)) .foregroundStyle(.secondary) } } .padding(.vertical, 2) } private var labels: some View { ForEach(issue.labels ?? [], id: \.self) { label in GBChip(label, colors.color(label)) } } } nonisolated enum IssueRoute: Hashable { case list(repo: String) case issue(repo: String, number: Int64) }