From ff9928c639f4b44046f63864829fff23172c3b22 Mon Sep 17 00:00:00 2001 From: Erik Doernenburg Date: Sun, 24 Mar 2024 20:44:37 +0100 Subject: [PATCH] Created basic version of edit sheet. Refactored pipeline builders. --- CCMenu/Source/Model/PipelineModel.swift | 14 +++--- .../AddCCTrayPipelineSheet.swift | 25 ++++------ .../CCTray Sheets/CCTrayPipelineBuilder.swift | 28 +++++------ .../Pipeline Window/EditPipelineSheet.swift | 46 ++++++++----------- .../AddGitHubPipelineSheet.swift | 26 +++-------- .../GitHub Sheets/GitHubAuthenticator.swift | 20 ++++++++ .../GitHub Sheets/GitHubPipelineBuilder.swift | 9 +--- .../Pipeline Window/PipelineListToolbar.swift | 3 +- CCMenuUITests/PipelineWindowTests.swift | 19 ++++++++ README.md | 12 +++-- 10 files changed, 109 insertions(+), 93 deletions(-) diff --git a/CCMenu/Source/Model/PipelineModel.swift b/CCMenu/Source/Model/PipelineModel.swift index e880387..1764334 100644 --- a/CCMenu/Source/Model/PipelineModel.swift +++ b/CCMenu/Source/Model/PipelineModel.swift @@ -19,10 +19,7 @@ final class PipelineModel: ObservableObject { } func update(pipeline: Pipeline) { - guard let idx = pipelines.firstIndex(where: { $0.id == pipeline.id }) else { - debugPrint("trying to update unknown pipeline \(pipelines.debugDescription)") - return - } + guard let idx = pipelines.firstIndex(where: { $0.id == pipeline.id }) else { return } let change = StatusChange(pipeline: pipeline, previousStatus: pipelines[idx].status) pipelines[idx] = pipeline pipelines[idx].lastUpdated = Date() @@ -43,13 +40,18 @@ final class PipelineModel: ObservableObject { @discardableResult func add(pipeline newPipeline: Pipeline) -> Bool { - if pipelines.contains(where: {$0.id == newPipeline.id}){ + if pipelines.contains(where: { $0.id == newPipeline.id }) { return false } pipelines.append(newPipeline) return true } + func remove(pipelineId: String) { + guard let idx = pipelines.firstIndex(where: { $0.id == pipelineId }) else { return } + pipelines.remove(at: idx) + } + private func updateSettings() { // TODO: this is called, too, every time the status gets updated... let list = pipelines.map({ $0.asDictionaryForPersisting() }) @@ -78,7 +80,7 @@ final class PipelineModel: ObservableObject { } private func addCCMenu2Pipeline() { - let p0 = Pipeline(name: "ccmenu2 (build-and-test)", feed: Pipeline.Feed(type: .github, url: "https://api.github.com/repos/erikdoe/ccmenu2/actions/workflows/build-and-test.yaml/runs")) + let p0 = Pipeline(name: "ccmenu2 | build-and-test", feed: Pipeline.Feed(type: .github, url: "https://api.github.com/repos/erikdoe/ccmenu2/actions/workflows/build-and-test.yaml/runs")) pipelines.append(p0) } diff --git a/CCMenu/Source/Pipeline Window/CCTray Sheets/AddCCTrayPipelineSheet.swift b/CCMenu/Source/Pipeline Window/CCTray Sheets/AddCCTrayPipelineSheet.swift index d8e8e19..0a6ca22 100644 --- a/CCMenu/Source/Pipeline Window/CCTray Sheets/AddCCTrayPipelineSheet.swift +++ b/CCMenu/Source/Pipeline Window/CCTray Sheets/AddCCTrayPipelineSheet.swift @@ -48,10 +48,7 @@ struct AddCCTrayPipelineSheet: View { .autocorrectionDisabled(true) .onSubmit { if !url.isEmpty { - Task { - let c = !credential.isEmpty ? credential : nil - await projectList.updateProjects(url: $url, credential: c) - } + Task { await projectList.updateProjects(url: $url, credential: credentialOptional()) } } } @@ -63,7 +60,7 @@ struct AddCCTrayPipelineSheet: View { .accessibilityIdentifier("Project picker") .disabled(!projectList.selected.isValid) .onChange(of: projectList.selected) { _ in - pipelineBuilder.updateName(project: projectList.selected) + pipelineBuilder.setDefaultName(project: projectList.selected) } .padding(.bottom) @@ -71,7 +68,7 @@ struct AddCCTrayPipelineSheet: View { TextField("Display name:", text: $pipelineBuilder.name) .accessibilityIdentifier("Display name field") Button("Reset", systemImage: "arrowshape.turn.up.backward") { - pipelineBuilder.updateName(project: projectList.selected) + pipelineBuilder.setDefaultName(project: projectList.selected) } } .padding(.bottom) @@ -83,16 +80,7 @@ struct AddCCTrayPipelineSheet: View { } .keyboardShortcut(.cancelAction) Button("Apply") { - var feedUrl = url - if useBasicAuth && !credential.user.isEmpty { - feedUrl = CCTrayPipelineBuilder.setUser(credential.user, inURL: url) - do { - try Keychain().setPassword(credential.password, forURL: feedUrl) - } catch { - // TODO: Figure out what to do here – so many errors... - } - } - let p = pipelineBuilder.makePipeline(feedUrl: feedUrl, name: projectList.selected.name) + let p = pipelineBuilder.makePipeline(feedUrl: url, credential: credentialOptional(), project: projectList.selected) model.add(pipeline: p) presentation.dismiss() } @@ -104,6 +92,11 @@ struct AddCCTrayPipelineSheet: View { .frame(idealWidth: 450) .padding() } + + private func credentialOptional() -> HTTPCredential? { + (useBasicAuth && !credential.isEmpty) ? credential : nil + } + } diff --git a/CCMenu/Source/Pipeline Window/CCTray Sheets/CCTrayPipelineBuilder.swift b/CCMenu/Source/Pipeline Window/CCTray Sheets/CCTrayPipelineBuilder.swift index 85e0a12..390655c 100644 --- a/CCMenu/Source/Pipeline Window/CCTray Sheets/CCTrayPipelineBuilder.swift +++ b/CCMenu/Source/Pipeline Window/CCTray Sheets/CCTrayPipelineBuilder.swift @@ -10,26 +10,28 @@ import Foundation class CCTrayPipelineBuilder: ObservableObject { @Published var name: String = "" - func updateName(project: CCTrayProject) { - var newName = "" - if project.isValid { - newName.append(project.name) - } - name = newName + func setDefaultName(project: CCTrayProject) { + name = project.isValid ? project.name : "" } - func makePipeline(feedUrl: String, name: String) -> Pipeline { - // TODO: Consider what is the best place for this code and how much state it should be aware of - // (and see same comment in GitHubPipelineBuilder) - let feed = Pipeline.Feed(type:.cctray, url: feedUrl, name: name) - var p: Pipeline = Pipeline(name: self.name, feed: feed) + func makePipeline(feedUrl: String, credential: HTTPCredential?, project: CCTrayProject) -> Pipeline { + var feedUrl = feedUrl + if let credential { + feedUrl = setUser(credential.user, inURL: feedUrl) + do { + try Keychain().setPassword(credential.password, forURL: feedUrl) + } catch { + // TODO: Figure out what to do here – so many errors... + } + } + let feed = Pipeline.Feed(type: .cctray, url: feedUrl, name: project.name) + var p: Pipeline = Pipeline(name: name, feed: feed) p.status = Pipeline.Status(activity: .sleeping) p.status.lastBuild = Build(result: .unknown) return p } - static func setUser(_ user: String?, inURL urlString: String) -> String { - // TODO: Consider what is the best place for this code + private func setUser(_ user: String?, inURL urlString: String) -> String { guard let user, !user.isEmpty else { return urlString } diff --git a/CCMenu/Source/Pipeline Window/EditPipelineSheet.swift b/CCMenu/Source/Pipeline Window/EditPipelineSheet.swift index d372a39..b6f6f34 100644 --- a/CCMenu/Source/Pipeline Window/EditPipelineSheet.swift +++ b/CCMenu/Source/Pipeline Window/EditPipelineSheet.swift @@ -8,7 +8,8 @@ import SwiftUI struct EditPipelineSheet: View { - var pipeline: Pipeline + @State var pipeline: Pipeline + @State var name: String = "" @ObservedObject var model: PipelineModel @Environment(\.presentationMode) @Binding var presentation @@ -16,40 +17,31 @@ struct EditPipelineSheet: View { VStack { Text("Edit Pipeline") .font(.headline) - Text("missing") + .padding(.bottom) + Form { + TextField("Name:", text: $name) + .accessibilityIdentifier("Name field") + } + .padding(.bottom) HStack { Button("Cancel") { presentation.dismiss() } + .keyboardShortcut(.cancelAction) Button("Apply") { -// var p = Pipeline(name: "erikdoe/ocmock", feedUrl: "http://localhost:4567/cc.xml") -// p.status = Pipeline.Status(activity: .sleeping, lastBuild: Build(result: .success)) -// model.pipelines[editIndex] = p + pipeline.name = name + model.update(pipeline: pipeline) presentation.dismiss() } - .buttonStyle(DefaultButtonStyle()) + .keyboardShortcut(.defaultAction) + .disabled(pipeline.name.isEmpty) + } + .onAppear() { + name = pipeline.name } } - .padding(EdgeInsets(top: 10, leading:10, bottom: 10, trailing: 10)) - } -} - - -struct EditPipelineSheet_Previews: PreviewProvider { - static var previews: some View { - Group { -// AddPipelineSheet(model: makeViewModel()) - } - } - - static func makeViewModel() -> PipelineModel { - let model = PipelineModel() - - var p0 = Pipeline(name: "connectfour", feed: Pipeline.Feed(type: .cctray, url: "http://localhost:4567/cctray.xml")) - p0.status = Pipeline.Status(activity: .building, lastBuild: Build(result: .failure)) - model.pipelines = [p0] - return model + .frame(minWidth: 400) + .frame(idealWidth: 450) + .padding() } - } - diff --git a/CCMenu/Source/Pipeline Window/GitHub Sheets/AddGitHubPipelineSheet.swift b/CCMenu/Source/Pipeline Window/GitHub Sheets/AddGitHubPipelineSheet.swift index 1ca139e..36501a3 100644 --- a/CCMenu/Source/Pipeline Window/GitHub Sheets/AddGitHubPipelineSheet.swift +++ b/CCMenu/Source/Pipeline Window/GitHub Sheets/AddGitHubPipelineSheet.swift @@ -66,11 +66,9 @@ struct AddGitHubPipelineSheet: View { .accessibilityIdentifier("Repository picker") .disabled(!repositoryList.selected.isValid) .onChange(of: repositoryList.selected) { _ in - pipelineBuilder.updateName(repository: repositoryList.selected, workflow: workflowList.selected) + pipelineBuilder.setDefaultName(repository: repositoryList.selected, workflow: workflowList.selected) if repositoryList.selected.isValid { - Task { - await workflowList.updateWorkflows(owner: owner, repository: repositoryList.selected.name, token: authenticator.token) - } + Task { await workflowList.updateWorkflows(owner: owner, repository: repositoryList.selected.name, token: authenticator.token) } } else { workflowList.clearWorkflows() } @@ -84,7 +82,7 @@ struct AddGitHubPipelineSheet: View { .accessibilityIdentifier("Workflow picker") .disabled(!workflowList.selected.isValid) .onChange(of: workflowList.selected) { _ in - pipelineBuilder.updateName(repository: repositoryList.selected, workflow: workflowList.selected) + pipelineBuilder.setDefaultName(repository: repositoryList.selected, workflow: workflowList.selected) } .padding(.bottom) @@ -92,7 +90,7 @@ struct AddGitHubPipelineSheet: View { TextField("Display name:", text: $pipelineBuilder.name) .accessibilityIdentifier("Display name field") Button("Reset", systemImage: "arrowshape.turn.up.backward") { - pipelineBuilder.updateName(repository: repositoryList.selected, workflow: workflowList.selected) + pipelineBuilder.setDefaultName(repository: repositoryList.selected, workflow: workflowList.selected) } } .padding(.bottom) @@ -104,7 +102,7 @@ struct AddGitHubPipelineSheet: View { } .keyboardShortcut(.cancelAction) Button("Apply") { - let p = pipelineBuilder.makePipeline(owner: owner) + let p = pipelineBuilder.makePipeline(owner: owner, repository: repositoryList.selected, workflow: workflowList.selected) model.add(pipeline: p) presentation.dismiss() } @@ -116,20 +114,10 @@ struct AddGitHubPipelineSheet: View { .frame(idealWidth: 450) .padding() .onAppear() { - do { - let token = try Keychain().getToken(forService: "GitHub") - authenticator.token = token - authenticator.tokenDescription = token ?? "" - } catch { - } + authenticator.fetchTokenFromKeychain() } .onDisappear() { - guard let token = authenticator.token else { return } - do { - try Keychain().setToken(token, forService: "GitHub") - } catch { - // TODO: Figure out what to do here – so many errors... - } + authenticator.storeTokenInKeychain() } } diff --git a/CCMenu/Source/Pipeline Window/GitHub Sheets/GitHubAuthenticator.swift b/CCMenu/Source/Pipeline Window/GitHub Sheets/GitHubAuthenticator.swift index 3612277..630b68f 100644 --- a/CCMenu/Source/Pipeline Window/GitHub Sheets/GitHubAuthenticator.swift +++ b/CCMenu/Source/Pipeline Window/GitHub Sheets/GitHubAuthenticator.swift @@ -117,4 +117,24 @@ class GitHubAuthenticator: ObservableObject { NSWorkspace.shared.open(GitHubAPI.applicationsUrl()) } + func fetchTokenFromKeychain() { + do { + token = try Keychain().getToken(forService: "GitHub") + } catch { + // TODO: Figure out what to do here – so many errors... + token = nil + } + tokenDescription = token ?? "" + } + + + func storeTokenInKeychain() { + guard let token else { return } + do { + try Keychain().setToken(token, forService: "GitHub") + } catch { + // TODO: Figure out what to do here – so many errors... + } + } + } diff --git a/CCMenu/Source/Pipeline Window/GitHub Sheets/GitHubPipelineBuilder.swift b/CCMenu/Source/Pipeline Window/GitHub Sheets/GitHubPipelineBuilder.swift index 8f86225..c23b4d2 100644 --- a/CCMenu/Source/Pipeline Window/GitHub Sheets/GitHubPipelineBuilder.swift +++ b/CCMenu/Source/Pipeline Window/GitHub Sheets/GitHubPipelineBuilder.swift @@ -8,11 +8,9 @@ import Foundation import SwiftUI class GitHubPipelineBuilder: ObservableObject { - private var repository = GitHubRepository() - private var workflow = GitHubWorkflow() @Published var name: String = "" - func updateName(repository: GitHubRepository, workflow: GitHubWorkflow) { + func setDefaultName(repository: GitHubRepository, workflow: GitHubWorkflow) { var newName = "" if repository.isValid { newName.append(repository.name) @@ -20,13 +18,10 @@ class GitHubPipelineBuilder: ObservableObject { newName.append(String(format: " | %@", workflow.name)) } } - self.repository = repository - self.workflow = workflow self.name = newName } - func makePipeline(owner: String) -> Pipeline { - // TODO: Consider what is the best place for this code and how much state it should be aware of + func makePipeline(owner: String, repository: GitHubRepository, workflow: GitHubWorkflow) -> Pipeline { let url = GitHubAPI.feedUrl(owner: owner, repository: repository.name, workflow: workflow.filename) let feed = Pipeline.Feed(type: .github, url:url) let pipeline = Pipeline(name: name, feed: feed) diff --git a/CCMenu/Source/Pipeline Window/PipelineListToolbar.swift b/CCMenu/Source/Pipeline Window/PipelineListToolbar.swift index 6ea53d1..5f69e23 100644 --- a/CCMenu/Source/Pipeline Window/PipelineListToolbar.swift +++ b/CCMenu/Source/Pipeline Window/PipelineListToolbar.swift @@ -72,6 +72,7 @@ struct PipelineListToolbar: ToolbarContent { .frame(height: 28) .opacity(0.7) .background() { + // TODO: Fix transparency in dark mode Color(.unemphasizedSelectedContentBackgroundColor).opacity(isHoveringOverAddMenu ? 0.45 : 0) } .onHover { @@ -92,7 +93,7 @@ struct PipelineListToolbar: ToolbarContent { Button() { withAnimation { - model.pipelines.removeAll(where: { viewState.selection.contains($0.id) }) + viewState.selection.forEach({ model.remove(pipelineId: $0) }) viewState.selection.removeAll() } } label: { diff --git a/CCMenuUITests/PipelineWindowTests.swift b/CCMenuUITests/PipelineWindowTests.swift index 91d2ceb..e73e25a 100644 --- a/CCMenuUITests/PipelineWindowTests.swift +++ b/CCMenuUITests/PipelineWindowTests.swift @@ -69,4 +69,23 @@ class PipelineWindowTests: XCTestCase { XCTAssertTrue(window.tables.staticTexts["connectfour"].exists == false) } + func testRenamesPipeline() throws { + let app = TestHelper.launchApp(pipelines: "CCTrayPipeline.json") + let window = app.windows["Pipelines"] + let sheet = window.sheets.firstMatch + + window.tables.staticTexts["connectfour"].click() + window.toolbars.buttons["Edit pipeline"].click() + + let nameField = sheet.textFields["Name field"] + nameField.click() + sheet.typeKey("a", modifierFlags: [ .command ]) + sheet.typeText("TEST-TEST-TEST") + sheet.buttons["Apply"].click() + + let titleText = window.tables.staticTexts["Pipeline title"] + expectation(for: NSPredicate(format: "value == 'TEST-TEST-TEST'"), evaluatedWith: titleText) + waitForExpectations(timeout: 2) + } + } diff --git a/README.md b/README.md index 164e2d1..39c36b5 100644 --- a/README.md +++ b/README.md @@ -42,20 +42,24 @@ For now the roadmap is tracked in this readme file. ### Pre-release 5 (planned) - [X] Optimised CCTray reader requests -- [ ] Edit pipelines +- [X] Edit pipelines - [X] Remaining menu appearance options - [X] Reduced polling frequency on low data connections -### Later +### Pre-release 6 (planned) -- [ ] Sounds - [ ] Import and export of pipeline config +- [ ] Set user/password for CCTray pipelines +- [ ] Refresh GitHub token -### To consider +### To consider +- [ ] Sounds +- [ ] Support for workflow-specific GitHub tokens - [ ] Improve accessibility - [ ] Add support for localisation - [ ] Show avatar in notifications (committer or repo owner) +- [ ] Support for log in with GitHub (is this even possible?) - [ ] Support for GitHub apps - [ ] Pipeline groups with submenus - [ ] Add Nevergreen-style dashboard (full screen window)