gitbay/Views/Issues/IssueListView.swift

5411753023a5e2958f17b523903abce380941107
gitbay-ios/gitbay/Views/Issues/IssueListView.swift history · blame · raw

114 lines · 4086 bytes

  1import SwiftUI
  2
  3struct IssueListView: View {
  4
  5    @State private var model: IssueListViewModel
  6    @State private var createModel: IssueCreateViewModel
  7    @State private var composing = false
  8    @State private var draftTitle = ""
  9    @State private var draftBody = ""
 10
 11    init(client: GitbayClient, repo: String) {
 12        _model = State(initialValue: IssueListViewModel(client: client, repoPath: repo))
 13        _createModel = State(initialValue: IssueCreateViewModel(client: client, repoPath: repo))
 14    }
 15
 16    var body: some View {
 17        List {
 18            Picker("State", selection: Bindable(model).filter) {
 19                ForEach(IssueListViewModel.StateFilter.allCases) { filter in
 20                    Text(filter.rawValue.capitalized).tag(filter)
 21                }
 22            }
 23            .pickerStyle(.segmented)
 24            .listRowBackground(Color.clear)
 25            .listRowInsets(EdgeInsets())
 26
 27            ForEach(model.state.value ?? []) { issue in
 28                NavigationLink(value: IssueRoute.issue(repo: model.repoPath, number: issue.number)) {
 29                    IssueRow(issue: issue)
 30                }
 31            }
 32            PageFooter(list: model.list)
 33        }
 34        .overlay { LoadStateOverlay(state: model.state) }
 35        .navigationTitle("Issues")
 36        .navigationBarTitleDisplayMode(.inline)
 37        .toolbar {
 38            ToolbarItem(placement: .topBarTrailing) {
 39                Button {
 40                    composing = true
 41                } label: {
 42                    Image(systemName: "plus")
 43                }
 44                .accessibilityIdentifier("issue-create-button")
 45            }
 46        }
 47        .sheet(isPresented: $composing) {
 48            ComposeSheet(
 49                heading: "New Issue",
 50                submitLabel: "Create",
 51                working: createModel.working,
 52                errorMessage: createModel.errorMessage,
 53                title: $draftTitle,
 54                bodyText: $draftBody
 55            ) {
 56                Task {
 57                    if await createModel.create(title: draftTitle, body: draftBody) != nil {
 58                        draftTitle = ""
 59                        draftBody = ""
 60                        composing = false
 61                        await model.load()
 62                    }
 63                }
 64            }
 65        }
 66        .task { await model.load() }
 67        .refreshable { await model.load() }
 68    }
 69}
 70
 71private struct IssueRow: View {
 72    let issue: Issue
 73
 74    var body: some View {
 75        VStack(alignment: .leading, spacing: 4) {
 76            HStack(alignment: .firstTextBaseline, spacing: 6) {
 77                Text("#\(issue.number)")
 78                    .font(.caption.monospaced())
 79                    .foregroundStyle(.secondary)
 80                Text(issue.title)
 81                    .font(.subheadline.weight(.medium))
 82                    .lineLimit(2)
 83            }
 84            HStack(spacing: 6) {
 85                Image(systemName: issue.isOpen ? "circle" : "checkmark.circle.fill")
 86                    .font(.caption2)
 87                    .foregroundStyle(issue.isOpen ? .green : .purple)
 88                ForEach(issue.labels ?? [], id: \.self) { label in
 89                    Text(label)
 90                        .font(.caption2)
 91                        .padding(.horizontal, 5)
 92                        .padding(.vertical, 1)
 93                        .background(.quaternary, in: Capsule())
 94                }
 95                Spacer()
 96                if let assignees = issue.assignees, !assignees.isEmpty {
 97                    Text(assignees.joined(separator: ", "))
 98                        .font(.caption)
 99                        .foregroundStyle(.secondary)
100                        .lineLimit(1)
101                }
102                Text(issue.createdAt, format: .relative(presentation: .named))
103                    .font(.caption)
104                    .foregroundStyle(.tertiary)
105            }
106        }
107        .padding(.vertical, 2)
108    }
109}
110
111nonisolated enum IssueRoute: Hashable {
112    case list(repo: String)
113    case issue(repo: String, number: Int64)
114}