fix: show graceful empty state when repository has issues turned off

This commit is contained in:
secana 2026-06-11 21:58:21 +02:00
commit 617e687e7b
3 changed files with 74 additions and 11 deletions

View file

@ -9,6 +9,7 @@ final class PaginationState<Item> {
private(set) var hasLoaded = false
var errorMessage: String?
var showError = false
var notFound = false
private var currentPage = 1
private var loadTask: Task<Void, Never>?
@ -26,10 +27,11 @@ final class PaginationState<Item> {
self.pageSize = pageSize
}
init(items: [Item], hasMore: Bool = false, pageSize: Int = 20) {
init(items: [Item], hasMore: Bool = false, pageSize: Int = 20, notFound: Bool = false) {
self.items = items
self.hasMore = hasMore
self.pageSize = pageSize
self.notFound = notFound
}
@discardableResult
@ -44,6 +46,7 @@ final class PaginationState<Item> {
hasLoaded = false
isLoading = true
showError = false
notFound = false
let pageSize = pageSize
let task = Task {
do {

View file

@ -38,6 +38,12 @@ struct IssueListView: View {
List {
if pagination.isLoading, pagination.items.isEmpty {
LoadingListSection()
} else if pagination.notFound {
ContentUnavailableView {
Label("Issues Unavailable", systemImage: "exclamationmark.circle")
} description: {
Text("Issues are not available for this repository.")
}
} else if pagination.items.isEmpty {
ContentUnavailableView {
Label(
@ -116,13 +122,18 @@ struct IssueListView: View {
private func reloadIssues(clearItems: Bool = false) -> Task<Void, Never> {
guard let issueService else { return Task {} }
return pagination.reload(clearItems: clearItems) { [self] page, limit in
try await issueService.fetchIssues(
owner: owner,
repo: repo,
state: stateFilter.rawValue,
page: page,
limit: limit,
)
do {
return try await issueService.fetchIssues(
owner: owner,
repo: repo,
state: stateFilter.rawValue,
page: page,
limit: limit,
)
} catch let error as ServiceError where error.httpStatusCode == 404 {
pagination.notFound = true
return []
}
}
}
@ -140,11 +151,12 @@ struct IssueListView: View {
}
#if DEBUG
init(preview _: Void, repository: Repository, authService: AuthenticationService, issues: [Issue]) {
init(preview _: Void, repository: Repository, authService: AuthenticationService, issues: [Issue],
notFound: Bool = false) {
self.repository = repository
self.authService = authService
issueService = nil
_pagination = State(initialValue: PaginationState(items: issues))
_pagination = State(initialValue: PaginationState(items: issues, notFound: notFound))
}
#endif
}
@ -160,6 +172,18 @@ struct IssueListView: View {
)
}
}
#Preview("Issues Unavailable") {
NavigationStack {
IssueListView(
preview: (),
repository: .preview,
authService: .previewDefault,
issues: [],
notFound: true,
)
}
}
#endif
struct IssueRow: View {

View file

@ -1,3 +1,4 @@
// swiftlint:disable file_length
import Foundation
import Testing
@testable import Forji
@ -359,6 +360,41 @@ struct PaginationStateHasLoadedTests {
}
}
// MARK: - notFound
struct PaginationStateNotFoundTests {
@Test @MainActor func notFoundFalseInitially() {
let pagination = PaginationState<String>(pageSize: 5)
#expect(!pagination.notFound)
}
@Test @MainActor func notFoundSetByFetchClosureShowsNoAlert() async {
let pagination = PaginationState<String>(pageSize: 20)
await pagination.reload { _, _ in
pagination.notFound = true
return []
}.value
#expect(pagination.notFound)
#expect(pagination.items.isEmpty)
#expect(!pagination.showError)
#expect(pagination.hasLoaded)
}
@Test @MainActor func reloadClearsNotFound() async {
let pagination = PaginationState<String>(pageSize: 20)
await pagination.reload { _, _ in
pagination.notFound = true
return []
}.value
#expect(pagination.notFound)
await pagination.reload { _, _ in ["ok"] }.value
#expect(!pagination.notFound)
#expect(pagination.items == ["ok"])
}
}
// MARK: - loadMore
struct PaginationStateLoadMoreTests {
@ -368,7 +404,7 @@ struct PaginationStateLoadMoreTests {
await pagination.reload { _, _ in ["a", "b"] }.value
#expect(pagination.hasMore)
await pagination.loadMore { page, limit in
await pagination.loadMore { page, _ in
#expect(page == 2)
return ["c", "d"]
}