gitbay/Views/Issues/IssueListView.swift

v1.4.0
gitbay-ios/gitbay/Views/Issues/IssueListView.swift history · blame · raw

149 lines · 5596 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 filtering = false
  9    @State private var draftTitle = ""
 10    @State private var draftBody = ""
 11    @State private var draftFormat = "md"
 12
 13    init(client: GitbayClient, repo: String) {
 14        _model = State(initialValue: IssueListViewModel(client: client, repoPath: repo))
 15        _createModel = State(initialValue: IssueCreateViewModel(client: client, repoPath: repo))
 16    }
 17
 18    var body: some View {
 19        List {
 20            Picker("State", selection: Bindable(model).filter.state) {
 21                ForEach(IssueFilter.State.allCases) { filter in
 22                    Text(filter.label).tag(filter)
 23                }
 24            }
 25            .pickerStyle(.segmented)
 26            .listRowBackground(Color.clear)
 27            .listRowInsets(EdgeInsets())
 28
 29            ForEach(model.state.value ?? []) { issue in
 30                NavigationLink(value: IssueRoute.issue(repo: model.repoPath, number: issue.number)) {
 31                    IssueRow(issue: issue, colors: model.colors)
 32                }
 33            }
 34            PageFooter(list: model.list)
 35        }
 36        .overlay { LoadStateOverlay(state: model.state) }
 37        .searchable(text: Bindable(model).filter.search, prompt: "Search titles and bodies")
 38        .navigationTitle("Issues")
 39        .navigationBarTitleDisplayMode(.inline)
 40        .toolbar {
 41            ToolbarItem(placement: .topBarTrailing) {
 42                Button {
 43                    filtering = true
 44                } label: {
 45                    Image(systemName: model.filter.isNarrowed
 46                        ? "line.3.horizontal.decrease.circle.fill"
 47                        : "line.3.horizontal.decrease.circle")
 48                }
 49                .accessibilityIdentifier("issue-filter-button")
 50            }
 51            ToolbarItem(placement: .topBarTrailing) {
 52                Button {
 53                    composing = true
 54                } label: {
 55                    Image(systemName: "plus")
 56                }
 57                .accessibilityIdentifier("issue-create-button")
 58            }
 59        }
 60        .sheet(isPresented: $filtering) {
 61            FilterSheet(
 62                title: "Filter Issues",
 63                author: Bindable(model).filter.author,
 64                milestone: Bindable(model).filter.milestone,
 65                milestones: model.milestones,
 66                onClear: {
 67                    let state = model.filter.state
 68                    model.filter = IssueFilter(state: state)
 69                }
 70            ) {
 71                IssueFilterExtra(
 72                    label: Bindable(model).filter.label,
 73                    assignee: Bindable(model).filter.assignee,
 74                    labels: model.labels,
 75                    colors: model.colors
 76                )
 77            }
 78            .task { await model.loadMilestones() }
 79        }
 80        .sheet(isPresented: $composing) {
 81            ComposeSheet(
 82                heading: "New Issue",
 83                submitLabel: "Create",
 84                working: createModel.working,
 85                errorMessage: createModel.errorMessage,
 86                title: $draftTitle,
 87                bodyText: $draftBody,
 88                format: $draftFormat,
 89                templates: createModel.templates
 90            ) {
 91                createModel.format = draftFormat
 92                Task {
 93                    if await createModel.create(title: draftTitle, body: draftBody) != nil {
 94                        draftTitle = ""
 95                        draftBody = ""
 96                        draftFormat = "md"
 97                        composing = false
 98                        await model.load()
 99                    }
100                }
101            }
102        }
103        .task { await model.loadIfNeeded() }
104        .task { await createModel.loadTemplates() }
105        .refreshable { await model.load() }
106    }
107}
108
109private struct IssueRow: View {
110    let issue: Issue
111    let colors: LabelColors
112
113    var body: some View {
114        VStack(alignment: .leading, spacing: 4) {
115            HStack(alignment: .firstTextBaseline, spacing: 6) {
116                Text("#\(issue.number)")
117                    .font(.gbMono(.caption))
118                    .foregroundStyle(.secondary)
119                Text(issue.title)
120                    .font(.gbSans(.subheadline).weight(.medium))
121                    .lineLimit(2)
122            }
123            HStack(spacing: 6) {
124                Image(systemName: issue.isOpen ? "circle" : "checkmark.circle.fill")
125                    .font(.gbSans(.caption2))
126                    .foregroundStyle(issue.isOpen ? Color.gbOK : Color.gbBad)
127                ForEach(issue.labels ?? [], id: \.self) { label in
128                    GBChip(label, colors.color(label))
129                }
130                Spacer()
131                if let assignees = issue.assignees, !assignees.isEmpty {
132                    Text(assignees.joined(separator: ", "))
133                        .font(.gbSans(.caption))
134                        .foregroundStyle(.secondary)
135                        .lineLimit(1)
136                }
137                Text(issue.createdAt, format: .relative(presentation: .named))
138                    .font(.gbSans(.caption))
139                    .foregroundStyle(.secondary)
140            }
141        }
142        .padding(.vertical, 2)
143    }
144}
145
146nonisolated enum IssueRoute: Hashable {
147    case list(repo: String)
148    case issue(repo: String, number: Int64)
149}