import ByreAPI import SwiftUI private enum Editing: Equatable { case none, new, existing(String) } struct WorkflowsView: View { @ViewState private var editing = Editing.none var body: some View { switch editing { case .none: list case .new: WorkflowEditor(original: nil) { editing = .none } case .existing(let name): WorkflowEditor(original: name) { editing = .none } .id(name) } } private var list: some View { ScrollView { VStack(alignment: .leading, spacing: 0) { HStack(alignment: .firstTextBaseline, spacing: 12) { Text("Workflows run a brief through stages, each its own task. Run asks your lead to start one, so you can add the project and the brief first.") .foregroundStyle(Palette.muted) .fixedSize(horizontal: false, vertical: true) .frame(maxWidth: .infinity, alignment: .leading) Button("New") { editing = .new } .buttonStyle(SmallLinkButtonStyle()) .help("Write a new workflow") } .padding(.top, 16) WorkflowList { editing = .existing($0) } } .padding(.horizontal, 16) .padding(.top, 8) .padding(.bottom, 16) .frame(maxWidth: .infinity, alignment: .leading) } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Palette.bg) } } private struct WorkflowList: View { let edit: (String) -> Void @Environment(Store.self) private var store @ViewState private var list: [WorkflowSummary]? @ViewState private var failure: String? @ViewState private var loads = 0 @ViewState private var off: (() -> Void)? var body: some View { VStack(alignment: .leading, spacing: 0) { if let list { if list.isEmpty { hint("No saved workflows. Ask your lead to write one.") } else { ForEach(list.sorted { $0.name.localizedStandardCompare($1.name) == .orderedAscending }, id: \.name) { w in WorkflowRow(workflow: w) { edit(w.name) } } } } else if let failure { hint(failure) } } .padding(.top, 8) .onAppear { off = store.events.add { ev in if ev.kind == "workflow.updated" || ev.kind == "workflow.deleted" { load() } } load() } .onDisappear { off?() off = nil loads += 1 } } private func hint(_ text: String) -> some View { Text(text).foregroundStyle(Palette.muted) } private func load() { loads += 1 let n = loads Task { do { let loaded = try await store.api.client.listWorkflows().ok.body.json if n == loads { list = loaded } } catch { failure = errorText(error) list = nil } } } } private struct WorkflowRow: View { let workflow: WorkflowSummary let edit: () -> Void @Environment(Store.self) private var store @Environment(Overlays.self) private var overlays var body: some View { HStack(alignment: .firstTextBaseline, spacing: 12) { VStack(alignment: .leading, spacing: 2) { Text(workflow.name) .font(Fonts.font(13, weight: 700)) if !workflow.description.isEmpty { Text(workflow.description) .foregroundStyle(Palette.muted) } } .textSelection(.enabled) .frame(maxWidth: .infinity, alignment: .leading) .fixedSize(horizontal: false, vertical: true) Button("Edit") { edit() } .buttonStyle(SmallLinkButtonStyle()) .help("See and change this workflow's steps and prompts") 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) { Rectangle().fill(Palette.border).frame(height: 1) } } 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: " let draft = store.drafts?.composerText(key) ?? "" store.drafts?.setComposerText(key, draft.isEmpty ? ask : "\(draft)\n\n\(ask)") overlays.leftTab = .lead overlays.leadFocus += 1 } }