a native ios client for gitbay

client ios swift

https://gitbay.org

gitbay/Views/Issues/IssueListView.swift

ui-smoke
gitbay-ios/gitbay/Views/Issues/IssueListView.swift history · blame · raw

110 lines · 3940 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(.gbMono(.caption))
 79                    .foregroundStyle(.secondary)
 80                Text(issue.title)
 81                    .font(.gbSans(.subheadline).weight(.medium))
 82                    .lineLimit(2)
 83            }
 84            HStack(spacing: 6) {
 85                Image(systemName: issue.isOpen ? "circle" : "checkmark.circle.fill")
 86                    .font(.gbSans(.caption2))
 87                    .foregroundStyle(issue.isOpen ? Color.gbOK : Color.gbBad)
 88                ForEach(issue.labels ?? [], id: \.self) { label in
 89                    GBChip(label, .secondary)
 90                }
 91                Spacer()
 92                if let assignees = issue.assignees, !assignees.isEmpty {
 93                    Text(assignees.joined(separator: ", "))
 94                        .font(.gbSans(.caption))
 95                        .foregroundStyle(.secondary)
 96                        .lineLimit(1)
 97                }
 98                Text(issue.createdAt, format: .relative(presentation: .named))
 99                    .font(.gbSans(.caption))
100                    .foregroundStyle(.secondary)
101            }
102        }
103        .padding(.vertical, 2)
104    }
105}
106
107nonisolated enum IssueRoute: Hashable {
108    case list(repo: String)
109    case issue(repo: String, number: Int64)
110}