Drop workflow kinds, deliver reviews when done, rename/delete workflows
This commit is contained in:
@@ -139,7 +139,7 @@ private struct ReviewWorkflowMenu: View {
|
||||
.help("The review workflow whose reviewers review every snapshot of this project's tasks")
|
||||
.task {
|
||||
do {
|
||||
reviewWorkflows = try await store.api.client.listWorkflows().ok.body.json.filter { $0.kind == .review }.map(\.name)
|
||||
reviewWorkflows = try await store.api.client.listWorkflows().ok.body.json.map(\.name)
|
||||
} catch {
|
||||
reviewWorkflows = []
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@ final class SwitcherModel {
|
||||
self.anchor = anchor
|
||||
Task {
|
||||
do {
|
||||
workflows = try await store.api.client.listWorkflows().ok.body.json.filter { $0.kind == .task }
|
||||
workflows = try await store.api.client.listWorkflows().ok.body.json
|
||||
} catch {
|
||||
overlays.showAlert(errorText(error))
|
||||
}
|
||||
|
||||
@@ -92,6 +92,28 @@ private func newStage(_ root: StepNode, _ model: String) -> StepNode {
|
||||
return s
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func stepGraph(_ root: StepNode) -> Flowchart {
|
||||
var chart = Flowchart(direction: .td)
|
||||
func walk(_ node: StepNode, _ before: [String]) -> [String] {
|
||||
switch node.kind {
|
||||
case .order:
|
||||
return node.children.reduce(before) { walk($1, $0) }
|
||||
case .parallel:
|
||||
return node.children.flatMap { walk($0, before) }
|
||||
case .stage:
|
||||
let id = "n\(chart.order.count)"
|
||||
chart.node(id, label: node.name.isEmpty ? " " : node.name, shape: .box)
|
||||
for b in before {
|
||||
chart.edges.append(FlowEdge(from: b, to: id, label: nil, style: .solid, arrow: true))
|
||||
}
|
||||
return [id]
|
||||
}
|
||||
}
|
||||
_ = walk(root, [])
|
||||
return chart
|
||||
}
|
||||
|
||||
private struct StepLine: Identifiable {
|
||||
let node: StepNode
|
||||
let guide: String
|
||||
@@ -118,7 +140,6 @@ struct WorkflowEditor: View {
|
||||
@ViewState private var loaded: WorkflowDefinition?
|
||||
@ViewState private var name = ""
|
||||
@ViewState private var description = ""
|
||||
@ViewState private var review = false
|
||||
@ViewState private var failure = ""
|
||||
@ViewState private var saving = false
|
||||
|
||||
@@ -139,30 +160,21 @@ struct WorkflowEditor: View {
|
||||
}
|
||||
}
|
||||
.buttonStyle(WebButtonStyle(primary: true))
|
||||
.disabled(saving || !changed || name.trimmingCharacters(in: .whitespaces).isEmpty)
|
||||
.disabled(saving || !changed || target.isEmpty)
|
||||
}
|
||||
.padding(.top, 16)
|
||||
if let root {
|
||||
if original == nil {
|
||||
TextField("Name", text: $name)
|
||||
.modifier(FieldStyle())
|
||||
.padding(.top, 12)
|
||||
} else {
|
||||
Text(name)
|
||||
.font(Fonts.font(13, weight: 700))
|
||||
.textSelection(.enabled)
|
||||
.padding(.top, 12)
|
||||
}
|
||||
TextField("Name", text: $name)
|
||||
.modifier(FieldStyle())
|
||||
.padding(.top, 12)
|
||||
TextField("Description", text: $description, axis: .vertical)
|
||||
.modifier(FieldStyle())
|
||||
.padding(.top, 8)
|
||||
Picker("", selection: $review) {
|
||||
Text("Runs on a brief, each stage its own task").tag(false)
|
||||
Text("Reviews snapshots, each stage a reviewer").tag(true)
|
||||
if !root.stageNames().isEmpty {
|
||||
FlowchartCanvas(layout: layoutFlowchart(stepGraph(root)))
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.top, 12)
|
||||
}
|
||||
.pickerStyle(.radioGroup)
|
||||
.labelsHidden()
|
||||
.padding(.top, 8)
|
||||
if !failure.isEmpty {
|
||||
Text(failure)
|
||||
.foregroundStyle(Palette.danger)
|
||||
@@ -197,11 +209,15 @@ struct WorkflowEditor: View {
|
||||
}
|
||||
|
||||
private var current: WorkflowDefinition? {
|
||||
root.map { WorkflowDefinition(description: description.isEmpty ? nil : description, kind: review ? .review : nil, steps: $0.children.map(\.step)) }
|
||||
root.map { WorkflowDefinition(description: description.isEmpty ? nil : description, steps: $0.children.map(\.step)) }
|
||||
}
|
||||
|
||||
private var target: String {
|
||||
name.trimmingCharacters(in: .whitespaces)
|
||||
}
|
||||
|
||||
private var changed: Bool {
|
||||
original == nil || current != loaded
|
||||
original == nil || current != loaded || target != original
|
||||
}
|
||||
|
||||
private func load() async {
|
||||
@@ -215,7 +231,6 @@ struct WorkflowEditor: View {
|
||||
let def = try await store.api.client.getWorkflow(path: .init(name: original), headers: .init(accept: [.init(contentType: .json)])).ok.body.json
|
||||
name = original
|
||||
description = def.description ?? ""
|
||||
review = def.kind == .review
|
||||
root = StepNode(kind: .order, children: def.steps.map { StepNode($0) })
|
||||
loaded = current
|
||||
} catch {
|
||||
@@ -229,14 +244,9 @@ struct WorkflowEditor: View {
|
||||
}
|
||||
saving = true
|
||||
defer { saving = false }
|
||||
let target = name.trimmingCharacters(in: .whitespaces)
|
||||
do {
|
||||
_ = try await store.api.client.putWorkflow(path: .init(name: target), body: .json(def)).ok.body.json
|
||||
failure = ""
|
||||
loaded = def
|
||||
if original == nil {
|
||||
close()
|
||||
}
|
||||
_ = try await store.api.client.putWorkflow(path: .init(name: target), query: .init(from: original), body: .json(def)).ok.body.json
|
||||
close()
|
||||
} catch {
|
||||
failure = errorText(error)
|
||||
}
|
||||
@@ -338,10 +348,12 @@ private struct StepRow: View {
|
||||
Button("Ungroup") { ungroup() }
|
||||
}
|
||||
Divider()
|
||||
let siblings = node.parent?.children ?? []
|
||||
let i = siblings.firstIndex { $0 === node }
|
||||
Button("Move up") { move(-1) }
|
||||
.disabled(node.index == 0)
|
||||
.disabled(i == nil || i == 0)
|
||||
Button("Move down") { move(1) }
|
||||
.disabled(node.index == node.parent!.children.count - 1)
|
||||
.disabled(i == nil || i == siblings.count - 1)
|
||||
Divider()
|
||||
Button("Remove") { node.detach() }
|
||||
} label: {
|
||||
|
||||
@@ -115,13 +115,8 @@ private struct WorkflowRow: View {
|
||||
var body: some View {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 12) {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 6) {
|
||||
Text(workflow.name)
|
||||
.font(Fonts.font(13, weight: 700))
|
||||
if workflow.kind == .review {
|
||||
Text("reviews snapshots").font(Fonts.font(11)).foregroundStyle(Palette.muted)
|
||||
}
|
||||
}
|
||||
Text(workflow.name)
|
||||
.font(Fonts.font(13, weight: 700))
|
||||
if !workflow.description.isEmpty {
|
||||
Text(workflow.description)
|
||||
.foregroundStyle(Palette.muted)
|
||||
@@ -135,13 +130,18 @@ private struct WorkflowRow: View {
|
||||
}
|
||||
.buttonStyle(SmallLinkButtonStyle())
|
||||
.help("See and change this workflow's steps and prompts")
|
||||
if workflow.kind == .task {
|
||||
Button("run") {
|
||||
run()
|
||||
}
|
||||
.buttonStyle(SmallLinkButtonStyle())
|
||||
.help("Open the Lead tab with a request to run this workflow in its composer")
|
||||
Button("run") {
|
||||
run()
|
||||
}
|
||||
.buttonStyle(SmallLinkButtonStyle())
|
||||
.help("Open the Lead tab with a request to run this workflow in its composer")
|
||||
Button("delete") {
|
||||
Task {
|
||||
await delete()
|
||||
}
|
||||
}
|
||||
.buttonStyle(SmallLinkButtonStyle())
|
||||
.help("Delete this workflow; tasks already running it keep their copy")
|
||||
}
|
||||
.padding(.vertical, 6)
|
||||
.overlay(alignment: .top) {
|
||||
@@ -149,6 +149,17 @@ private struct WorkflowRow: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func delete() async {
|
||||
guard await overlays.showConfirm(["Delete the workflow \"\(workflow.name)\"?"], action: "Delete", danger: true) else {
|
||||
return
|
||||
}
|
||||
do {
|
||||
_ = try await store.api.client.deleteWorkflow(path: .init(name: workflow.name)).noContent
|
||||
} catch {
|
||||
overlays.showAlert(errorText(error))
|
||||
}
|
||||
}
|
||||
|
||||
private func run() {
|
||||
let key = "message:lead"
|
||||
let ask = "Run the \"\(workflow.name)\" workflow: "
|
||||
|
||||
Reference in New Issue
Block a user