diff --git a/Classes/New Issue/GithubClient+Template.swift b/Classes/New Issue/GithubClient+Template.swift new file mode 100644 index 000000000..99dad64d8 --- /dev/null +++ b/Classes/New Issue/GithubClient+Template.swift @@ -0,0 +1,115 @@ +// +// GithubClient+Template.swift +// Freetime +// +// Created by Ehud Adler on 11/11/18. +// Copyright © 2018 Ryan Nystrom. All rights reserved. +// + +import Foundation +import GitHubAPI +import GitHubSession +import Squawk + +private let githubIssueURL = ".github/ISSUE_TEMPLATE" +let tempalateRegex = ".github/ISSUE_TEMPLATE" + +struct IssueTemplateRepoDetails { + let owner: String + let name: String + let defaultBranch: String +} + +extension GithubClient { + + private func fetchTemplateFile( + repoDetails: IssueTemplateRepoDetails, + filename: String, + testingFile: String? = nil, + completion: @escaping (Result) -> Void + ) { + + // For Testing. + if let testingFile = testingFile { completion(.success(testingFile)) } + + fetchFile( + owner: repoDetails.owner, + repo: repoDetails.name, + branch: repoDetails.defaultBranch, + path: "\(githubIssueURL)/\(filename)") { result in + switch result { + case .success(let file): completion(.success(file)) + case .error(let error): completion(.error(error)) + case .nonUTF8: completion(.error(nil)) + } + } + } + + func createTemplate( + repoDetails: IssueTemplateRepoDetails, + filename: String, + testingFile: String? = nil, + completion: @escaping (Result) -> Void + ) { + + fetchTemplateFile(repoDetails: repoDetails, filename: filename, testingFile: testingFile) { result in + switch result { + case .success(let file): + let nameAndDescription = IssueTemplateHelper.getNameAndDescription(fromTemplatefile: file) + if let name = nameAndDescription.name { + let cleanedFile = IssueTemplateHelper.cleanText(file: file) + completion(.success(IssueTemplate(title: name, template: cleanedFile))) + } else { + completion(.error(nil)) + } + case .error(let error): + completion(.error(error)) + } + } + } + + func createNewIssue( + repoDetails: IssueTemplateRepoDetails, + completion: @escaping (Result<[IssueTemplate]>) -> Void + ) { + + var templates: [IssueTemplate] = [] + + // Create group. + // We need this since we will be making multiple async calls inside a for-loop. + let templateGroup = DispatchGroup() + + fetchFiles( + owner: repoDetails.owner, + repo: repoDetails.name, + branch: repoDetails.defaultBranch, + path: githubIssueURL) { result in + switch result { + case .success(let files): + for file in files { + templateGroup.enter() + self.createTemplate(repoDetails: repoDetails, filename: file.name, completion: { result in + switch result { + case .success(let template): + templates.append(template) + default: break + // If error creating template continue silently + // Worst case is no templates are found and a blank issue is shown + } + templateGroup.leave() + }) + } + case .error(let error): + completion(.error(error)) + } + + // Wait for async calls in for-loop to finish up + templateGroup.notify(queue: .main) { + + // Sort lexicographically + let sortedTemplates = templates.sorted(by: {$0.title < $1.title }) + completion(.success(sortedTemplates)) + } + } + } +} diff --git a/Classes/New Issue/IssueTemplateHelper.swift b/Classes/New Issue/IssueTemplateHelper.swift new file mode 100644 index 000000000..30782d830 --- /dev/null +++ b/Classes/New Issue/IssueTemplateHelper.swift @@ -0,0 +1,50 @@ +// +// IssueTemplates.swift +// Freetime +// +// Created by Ehud Adler on 11/3/18. +// Copyright © 2018 Ryan Nystrom. All rights reserved. +// + +import Foundation +import GitHubAPI +import GitHubSession +import Squawk + +struct IssueTemplate { + let title: String + let template: String +} + +final class IssueTemplateHelper { + + static func getNameAndDescription(fromTemplatefile file: String) -> (name: String?, about: String?) { + let names = file.matches(regex: String.getRegexForLine(after: "name")) + let abouts = file.matches(regex: String.getRegexForLine(after: "about")) + let name = names.first?.trimmingCharacters(in: .whitespaces) + let about = abouts.first?.trimmingCharacters(in: .whitespaces) + return (name, about) + } + + static func cleanText(file: String) -> String { + + var cleanedFile = "" + // Remove all template detail text + // ----- + // name: + // about: + // ----- + if let textToClean = file.matches(regex: "([-]{3,})[^[-]{3,}]*([-]{3,})").first { + if let range = file.range(of: textToClean) { + cleanedFile = file.replacingOccurrences( + of: textToClean, + with: "", + options: .literal, + range: range + ) + } + cleanedFile = cleanedFile.trimmingCharacters(in: .whitespacesAndNewlines) + } + return cleanedFile + } +} diff --git a/Classes/New Issue/NewIssueTableViewController+Template.swift b/Classes/New Issue/NewIssueTableViewController+Template.swift new file mode 100644 index 000000000..f31ab34de --- /dev/null +++ b/Classes/New Issue/NewIssueTableViewController+Template.swift @@ -0,0 +1,80 @@ +// +// ViewController+Template.swift +// Freetime +// +// Created by Ehud Adler on 12/1/18. +// Copyright © 2018 Ryan Nystrom. All rights reserved. +// + +import UIKit +import GitHubSession + +extension NewIssueTableViewControllerDelegate where Self: UIViewController { + + private func _getTemplateIssueAlert( + with templates: [IssueTemplate], + session: GitHubUserSession?, + repoDetails: IssueTemplateRepoDetails + ) -> UIAlertController { + + let alertView = UIAlertController.configured( + title: Constants.Strings.newIssue, + message: NSLocalizedString("Choose Template", comment: ""), + preferredStyle: .actionSheet + ) + + for template in templates { + alertView.addAction( + UIAlertAction( + title: template.title, + style: .default, + handler: { _ in + guard let viewController = NewIssueTableViewController.create( + client: GithubClient(userSession: session), + owner: repoDetails.owner, + repo: repoDetails.name, + template: template.template, + signature: repoDetails.owner == Constants.GitHawk.owner ? .bugReport : .sentWithGitHawk + ) else { + assertionFailure("Failed to create NewIssueTableViewController") + return + } + viewController.delegate = self + let navController = UINavigationController(rootViewController: viewController) + navController.modalPresentationStyle = .formSheet + self.route_present(to: navController) + })) + } + + alertView.addAction( + UIAlertAction( + title: NSLocalizedString("Dismiss", comment: ""), + style: .cancel, + handler: { _ in + alertView.dismiss(animated: trueUnlessReduceMotionEnabled) + }) + ) + return alertView + } + + func getTemplateIssueAlert( + withTemplates sortedTemplates: [IssueTemplate], + session: GitHubUserSession? = nil, + details: IssueTemplateRepoDetails + ) -> UIAlertController { + + var templates = sortedTemplates + templates.append( + IssueTemplate( + title: NSLocalizedString("Regular Issue", comment: ""), + template: "" + ) + ) + let alertView = _getTemplateIssueAlert( + with: templates, + session: session, + repoDetails: details + ) + return alertView + } +} diff --git a/Classes/New Issue/NewIssueTableViewController.swift b/Classes/New Issue/NewIssueTableViewController.swift index 343f903e3..88154c191 100644 --- a/Classes/New Issue/NewIssueTableViewController.swift +++ b/Classes/New Issue/NewIssueTableViewController.swift @@ -51,6 +51,7 @@ final class NewIssueTableViewController: UITableViewController, UITextFieldDeleg @IBOutlet var titleField: UITextField! @IBOutlet var bodyField: UITextView! + private var template: String? private var client: GithubClient! private var owner: String! private var repo: String! @@ -69,10 +70,14 @@ final class NewIssueTableViewController: UITableViewController, UITextFieldDeleg return raw } - static func create(client: GithubClient, - owner: String, - repo: String, - signature: IssueSignatureType? = nil) -> NewIssueTableViewController? { + static func create( + client: GithubClient, + owner: String, + repo: String, + template: String? = nil, + signature: IssueSignatureType? = nil + ) -> NewIssueTableViewController? { + let storyboard = UIStoryboard(name: "NewIssue", bundle: nil) let viewController = storyboard.instantiateInitialViewController() as? NewIssueTableViewController @@ -80,6 +85,7 @@ final class NewIssueTableViewController: UITableViewController, UITextFieldDeleg viewController?.client = client viewController?.owner = owner viewController?.repo = repo + viewController?.template = template viewController?.signature = signature return viewController @@ -106,6 +112,7 @@ final class NewIssueTableViewController: UITableViewController, UITextFieldDeleg // Setup markdown input view bodyField.githawkConfigure(inset: false) setupInputView() + bodyField.text = template // Update title to use localization title = Constants.Strings.newIssue diff --git a/Classes/Repository/RepositoryViewController.swift b/Classes/Repository/RepositoryViewController.swift index 1402c9cb0..3513d86cf 100644 --- a/Classes/Repository/RepositoryViewController.swift +++ b/Classes/Repository/RepositoryViewController.swift @@ -184,7 +184,7 @@ EmptyViewDelegate { } self?.buildViewControllers() self?.reloadPagerTabStripView() - }) + }) } @objc func onNavigationTitle(sender: UIView) { @@ -234,23 +234,53 @@ EmptyViewDelegate { } } - func newIssueAction() -> UIAlertAction? { - guard case .value(let details) = state, - details.hasIssuesEnabled, - let newIssueViewController = NewIssueTableViewController.create( - client: client, + func newIssueAction(sender: UIView) -> UIAlertAction? { + + guard case .value(let details) = self.state else { return nil } + + let repoDetails = IssueTemplateRepoDetails( owner: repo.owner, - repo: repo.name, - signature: .sentWithGitHawk) - else { - return nil - } + name: repo.name, + defaultBranch: details.defaultBranch + ) - newIssueViewController.delegate = self - weak var weakSelf = self + let action = UIAlertAction(title: Constants.Strings.newIssue, style: .default) { _ in + self.client.createNewIssue(repoDetails: repoDetails) { [weak self] result in + guard let strongSelf = self else { return } + + switch result { + case .success(let templates): - return AlertAction(AlertActionBuilder { $0.rootViewController = weakSelf }) - .newIssue(issueController: newIssueViewController) + if templates.count > 0 { + let alertView = strongSelf.getTemplateIssueAlert( + withTemplates: templates, + details: repoDetails + ) + alertView.popoverPresentationController?.setSourceView(sender) + strongSelf.route_present(to: alertView) + } else { + + // No templates exists, show blank new issue view controller + guard let viewController = NewIssueTableViewController.create( + client: strongSelf.client, + owner: repoDetails.owner, + repo: repoDetails.name, + signature: .sentWithGitHawk + ) else { + assertionFailure("Failed to create NewIssueTableViewController") + return + } + viewController.delegate = strongSelf + let navController = UINavigationController(rootViewController: viewController) + navController.modalPresentationStyle = .formSheet + strongSelf.route_present(to: navController) + } + case .error(let error): + Squawk.show(error: error) + } + } + } + return action } func workingCopyAction() -> UIAlertAction? { @@ -265,7 +295,7 @@ EmptyViewDelegate { let title = NSLocalizedString("Working Copy", comment: "") let action = UIAlertAction(title: title, style: .default, handler: { _ in - UIApplication.shared.open(url) + UIApplication.shared.open(url) }) return action } @@ -281,8 +311,8 @@ EmptyViewDelegate { alert.addActions([ viewHistoryAction(owner: repo.owner, repo: repo.name, branch: branch, client: client), - newIssueAction() - ]) + newIssueAction(sender: sender) + ]) if let url = repoUrl { alert.add(action: AlertAction(alertBuilder).share([url], activities: [TUSafariActivity()], type: .shareUrl) { $0.popoverPresentationController?.setSourceView(sender) diff --git a/Classes/Settings/Settings.storyboard b/Classes/Settings/Settings.storyboard index 73e1793fb..16a4f9968 100644 --- a/Classes/Settings/Settings.storyboard +++ b/Classes/Settings/Settings.storyboard @@ -134,14 +134,14 @@ - + -