diff --git a/Classes/Labels/GitHubClient+RepositoryLabels.swift b/Classes/Labels/GitHubClient+RepositoryLabels.swift new file mode 100644 index 000000000..0191a7ad6 --- /dev/null +++ b/Classes/Labels/GitHubClient+RepositoryLabels.swift @@ -0,0 +1,62 @@ +// +// GitHubClient+RepositoryLabels.swift +// Freetime +// +// Created by Quentin Dreyer on 05/03/2020. +// Copyright © 2020 Ryan Nystrom. All rights reserved. +// + +import GitHubAPI +import Apollo + +private extension FetchRepositoryLabelsQuery.Data { + + func labels() -> [RepositoryLabel] { + var labels: [RepositoryLabel] = [] + repository?.labels.map { nodes in + nodes.nodes.map { node in + labels += node.compactMap { + guard let label = $0 else { return nil } + return RepositoryLabel(color: label.color, name: label.name) + } + } + } + return labels + } + + func nextPageToken() -> String? { + guard repository?.labels?.pageInfo.hasNextPage == true else { return nil } + return repository?.labels?.pageInfo.endCursor + } + +} + +extension GithubClient { + + struct RepositoryLabelsPayload { + let labels: [RepositoryLabel] + let nextPage: String? + } + + func fetchRepositoryLabels(owner: String, + repo: String, + nextPage: String?, + completion: @escaping (Result) -> Void + ) { + let query = FetchRepositoryLabelsQuery(owner: owner, repo: repo, after: nextPage) + client.query(query, result: { $0 }, completion: { result in + + switch result { + case .failure(let error): + completion(.error(error)) + + case .success(let data): + let payload = RepositoryLabelsPayload( + labels: data.labels(), + nextPage: data.nextPageToken() + ) + completion(.success(payload)) + } + }) + } +} diff --git a/Classes/Labels/LabelsViewController.swift b/Classes/Labels/LabelsViewController.swift index 502b75cba..ae3d963fe 100644 --- a/Classes/Labels/LabelsViewController.swift +++ b/Classes/Labels/LabelsViewController.swift @@ -16,8 +16,9 @@ LabelSectionControllerDelegate { private let selectedLabels: Set private var labels = [RepositoryLabel]() + private let owner: String + private let repo: String private let client: GithubClient - private let request: RepositoryLabelsQuery init( selected: [RepositoryLabel], @@ -27,7 +28,8 @@ LabelSectionControllerDelegate { ) { self.selectedLabels = Set(selected) self.client = client - self.request = RepositoryLabelsQuery(owner: owner, repo: repo) + self.owner = owner + self.repo = repo super.init(emptyErrorMessage: NSLocalizedString("No labels found", comment: "")) preferredContentSize = Styles.Sizes.contextMenuSize title = Constants.Strings.labels @@ -87,20 +89,20 @@ LabelSectionControllerDelegate { // MARK: Overrides override func fetch(page: String?) { - client.client.query(request, result: { data in - data.repository?.labels?.nodes - }, completion: { [weak self] result in + client.fetchRepositoryLabels( + owner: owner, + repo: repo, + nextPage: page as String? + ) { [weak self] result in + guard let strongSelf = self else { return } switch result { - case .success(let nodes): - self?.labels = nodes.compactMap { - guard let node = $0 else { return nil } - return RepositoryLabel(color: node.color, name: node.name) - }.sorted { $0.name < $1.name } - self?.update(animated: true) - case .failure(let error): + case .success(let payload): + self?.labels = payload.labels.sorted { $0.name < $1.name } + strongSelf.update(page: payload.nextPage, animated: true) + case .error(let error): Squawk.show(error: error) } - }) + } } // MARK: BaseListViewControllerDataSource diff --git a/Freetime.xcodeproj/project.pbxproj b/Freetime.xcodeproj/project.pbxproj index 1f6f303db..0d0c23a4b 100644 --- a/Freetime.xcodeproj/project.pbxproj +++ b/Freetime.xcodeproj/project.pbxproj @@ -459,6 +459,7 @@ 29FE635F21AE2E2F00A07A86 /* RepositoryLoadingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29FE635E21AE2E2F00A07A86 /* RepositoryLoadingViewController.swift */; }; 29FE636121AE2E7900A07A86 /* RepositoryErrorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29FE636021AE2E7900A07A86 /* RepositoryErrorViewController.swift */; }; 29FF85A51EE1EA7A007B8762 /* ReactionContent+ReactionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29FF85A41EE1EA7A007B8762 /* ReactionContent+ReactionType.swift */; }; + 2CDD97C22411B61C0016D5CF /* GitHubClient+RepositoryLabels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CDD97C12411B61C0016D5CF /* GitHubClient+RepositoryLabels.swift */; }; 3E79A2FF1F8A7DA700E1126B /* ShortcutHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E79A2FE1F8A7DA700E1126B /* ShortcutHandler.swift */; }; 4920F1A81F72E27200131E9D /* UIViewController+UserActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4920F1A71F72E27200131E9D /* UIViewController+UserActivity.swift */; }; 49AF91B1204B416500DFF325 /* MergeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49AF91B0204B416500DFF325 /* MergeTests.swift */; }; @@ -1060,6 +1061,7 @@ 29FE635E21AE2E2F00A07A86 /* RepositoryLoadingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoryLoadingViewController.swift; sourceTree = ""; }; 29FE636021AE2E7900A07A86 /* RepositoryErrorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoryErrorViewController.swift; sourceTree = ""; }; 29FF85A41EE1EA7A007B8762 /* ReactionContent+ReactionType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ReactionContent+ReactionType.swift"; sourceTree = ""; }; + 2CDD97C12411B61C0016D5CF /* GitHubClient+RepositoryLabels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GitHubClient+RepositoryLabels.swift"; sourceTree = ""; }; 36115D494E8C3B4F39AC8CD9 /* Pods-Freetime.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Freetime.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Freetime/Pods-Freetime.debug.xcconfig"; sourceTree = ""; }; 3E106824819769E0A6665A79 /* Pods-FreetimeWatch.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FreetimeWatch.release.xcconfig"; path = "Pods/Target Support Files/Pods-FreetimeWatch/Pods-FreetimeWatch.release.xcconfig"; sourceTree = ""; }; 3E79A2FE1F8A7DA700E1126B /* ShortcutHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutHandler.swift; sourceTree = ""; }; @@ -2154,6 +2156,7 @@ 2924C18A20D5B3A100FCFCFF /* LabelMenuCell.swift */, 29C8F9B4208C081D0075931C /* LabelSectionController.swift */, 2924C18C20D5B3DD00FCFCFF /* LabelsViewController.swift */, + 2CDD97C12411B61C0016D5CF /* GitHubClient+RepositoryLabels.swift */, ); path = Labels; sourceTree = ""; @@ -3263,6 +3266,7 @@ 031E0241220B433C00A329F1 /* UIImage+Color.swift in Sources */, 29999734203135E100995FFD /* IssueMergeContextCell.swift in Sources */, 29EDFE821F661562005BCCEB /* RepositoryReadmeModel.swift in Sources */, + 2CDD97C22411B61C0016D5CF /* GitHubClient+RepositoryLabels.swift in Sources */, 29EDFE841F661776005BCCEB /* RepositoryReadmeSectionController.swift in Sources */, 29136BDF200A7A75007317BE /* UIScrollView+LeftRightSafeInset.swift in Sources */, 298C7E2621D7F56600DD2A60 /* SettingsAccountCell.swift in Sources */, diff --git a/gql/API.swift b/gql/API.swift index 2b1bcdcf6..895fe7e27 100644 --- a/gql/API.swift +++ b/gql/API.swift @@ -1389,20 +1389,22 @@ public final class AddReactionMutation: GraphQLMutation { } } -public final class RepositoryLabelsQuery: GraphQLQuery { +public final class FetchRepositoryLabelsQuery: GraphQLQuery { public let operationDefinition = - "query RepositoryLabels($owner: String!, $repo: String!) {\n repository(owner: $owner, name: $repo) {\n __typename\n labels(first: 100) {\n __typename\n nodes {\n __typename\n name\n color\n }\n }\n }\n}" + "query fetchRepositoryLabels($owner: String!, $repo: String!, $after: String) {\n repository(owner: $owner, name: $repo) {\n __typename\n labels(first: 100, after: $after) {\n __typename\n nodes {\n __typename\n name\n color\n }\n pageInfo {\n __typename\n hasNextPage\n endCursor\n }\n }\n }\n}" public var owner: String public var repo: String + public var after: String? - public init(owner: String, repo: String) { + public init(owner: String, repo: String, after: String? = nil) { self.owner = owner self.repo = repo + self.after = after } public var variables: GraphQLMap? { - return ["owner": owner, "repo": repo] + return ["owner": owner, "repo": repo, "after": after] } public struct Data: GraphQLSelectionSet { @@ -1437,7 +1439,7 @@ public final class RepositoryLabelsQuery: GraphQLQuery { public static let selections: [GraphQLSelection] = [ GraphQLField("__typename", type: .nonNull(.scalar(String.self))), - GraphQLField("labels", arguments: ["first": 100], type: .object(Label.selections)), + GraphQLField("labels", arguments: ["first": 100, "after": GraphQLVariable("after")], type: .object(Label.selections)), ] public private(set) var resultMap: ResultMap @@ -1475,6 +1477,7 @@ public final class RepositoryLabelsQuery: GraphQLQuery { public static let selections: [GraphQLSelection] = [ GraphQLField("__typename", type: .nonNull(.scalar(String.self))), GraphQLField("nodes", type: .list(.object(Node.selections))), + GraphQLField("pageInfo", type: .nonNull(.object(PageInfo.selections))), ] public private(set) var resultMap: ResultMap @@ -1506,6 +1509,16 @@ public final class RepositoryLabelsQuery: GraphQLQuery { } } + /// Information to aid in pagination. + public var pageInfo: PageInfo { + get { + return PageInfo(unsafeResultMap: resultMap["pageInfo"]! as! ResultMap) + } + set { + resultMap.updateValue(newValue.resultMap, forKey: "pageInfo") + } + } + public struct Node: GraphQLSelectionSet { public static let possibleTypes = ["Label"] @@ -1555,6 +1568,55 @@ public final class RepositoryLabelsQuery: GraphQLQuery { } } } + + public struct PageInfo: GraphQLSelectionSet { + public static let possibleTypes = ["PageInfo"] + + public static let selections: [GraphQLSelection] = [ + GraphQLField("__typename", type: .nonNull(.scalar(String.self))), + GraphQLField("hasNextPage", type: .nonNull(.scalar(Bool.self))), + GraphQLField("endCursor", type: .scalar(String.self)), + ] + + public private(set) var resultMap: ResultMap + + public init(unsafeResultMap: ResultMap) { + self.resultMap = unsafeResultMap + } + + public init(hasNextPage: Bool, endCursor: String? = nil) { + self.init(unsafeResultMap: ["__typename": "PageInfo", "hasNextPage": hasNextPage, "endCursor": endCursor]) + } + + public var __typename: String { + get { + return resultMap["__typename"]! as! String + } + set { + resultMap.updateValue(newValue, forKey: "__typename") + } + } + + /// When paginating forwards, are there more items? + public var hasNextPage: Bool { + get { + return resultMap["hasNextPage"]! as! Bool + } + set { + resultMap.updateValue(newValue, forKey: "hasNextPage") + } + } + + /// When paginating forwards, the cursor to continue. + public var endCursor: String? { + get { + return resultMap["endCursor"] as? String + } + set { + resultMap.updateValue(newValue, forKey: "endCursor") + } + } + } } } } diff --git a/gql/RepositoryLabels.graphql b/gql/RepositoryLabels.graphql index 1ca72e7ef..171913f74 100644 --- a/gql/RepositoryLabels.graphql +++ b/gql/RepositoryLabels.graphql @@ -1,10 +1,14 @@ -query RepositoryLabels($owner: String!, $repo: String!) { +query fetchRepositoryLabels($owner: String!, $repo: String!, $after: String) { repository(owner: $owner, name: $repo) { - labels(first:100) { + labels(first:100, after: $after) { nodes { name color } - } + pageInfo { + hasNextPage + endCursor + } + } } }