krz/domain-dig

an ios app for DNS & SSL analysis

clone: git clone https://gitbay.org/krz/domain-dig.git

v4.5.0: DomainDig/WorkflowsView.swift · raw

  1import SwiftUI
  2
  3private enum WorkflowDestinationMode: String, CaseIterable, Identifiable {
  4    case existing
  5    case new
  6
  7    var id: String { rawValue }
  8}
  9
 10struct WorkflowsView: View {
 11    @Environment(\.appDensity) private var appDensity
 12    @Bindable var viewModel: DomainViewModel
 13    @State private var purchaseService = PurchaseService.shared
 14
 15    @State private var showingCreateWorkflow = false
 16
 17    var body: some View {
 18        let _ = purchaseService.currentTier
 19
 20        List {
 21            if let limitMessage = FeatureAccessService.workflowAllowanceSummary(currentCount: viewModel.workflows.count) {
 22                Section {
 23                    Text(limitMessage)
 24                        .font(appDensity.font(.caption))
 25                        .foregroundStyle(.secondary)
 26                }
 27                .listRowBackground(Color(.systemGray6).opacity(0.5))
 28            }
 29
 30            workflowContent
 31        }
 32        .scrollContentBackground(.hidden)
 33        .background(Color.black)
 34        .refreshable {
 35            await viewModel.refreshWorkflowList()
 36        }
 37        .navigationTitle("Workflows")
 38        .toolbar {
 39            ToolbarItemGroup(placement: .topBarTrailing) {
 40                Button {
 41                    showingCreateWorkflow = true
 42                } label: {
 43                    Image(systemName: "plus.circle")
 44                }
 45
 46                if !viewModel.workflows.isEmpty {
 47                    EditButton()
 48                }
 49            }
 50        }
 51        .sheet(isPresented: $showingCreateWorkflow) {
 52            WorkflowComposerView(viewModel: viewModel)
 53        }
 54        .sheet(item: workflowSummaryBinding) { summary in
 55            WorkflowRunSummaryView(viewModel: viewModel, summary: summary)
 56        }
 57        .preferredColorScheme(.dark)
 58    }
 59
 60    private var workflowSummaryBinding: Binding<WorkflowRunSummary?> {
 61        Binding(
 62            get: { viewModel.latestWorkflowRunSummary },
 63            set: { viewModel.latestWorkflowRunSummary = $0 }
 64        )
 65    }
 66
 67    @ViewBuilder
 68    private var workflowContent: some View {
 69        if viewModel.batchLookupSource == .workflow, (!viewModel.batchResults.isEmpty || viewModel.batchLookupRunning) {
 70            Section("Workflow Run") {
 71                VStack(alignment: .leading, spacing: 8) {
 72                    ProgressView(
 73                        value: Double(viewModel.batchCompletedCount),
 74                        total: Double(max(viewModel.batchTotalCount, 1))
 75                    )
 76                    .tint(.cyan)
 77
 78                    HStack {
 79                        Text(viewModel.batchProgressLabel)
 80                            .font(appDensity.font(.caption))
 81                            .foregroundStyle(.secondary)
 82                        Spacer()
 83                        if viewModel.batchLookupRunning {
 84                            Button("Cancel") {
 85                                viewModel.cancelBatchLookup()
 86                            }
 87                            .buttonStyle(.bordered)
 88                            .font(appDensity.font(.caption2))
 89                        }
 90                    }
 91                }
 92                .padding(.vertical, 4)
 93            }
 94            .listRowBackground(Color(.systemGray6).opacity(0.5))
 95        }
 96
 97        if viewModel.workflows.isEmpty {
 98            Section {
 99                EmptyStateCardView(
100                    title: "No Workflows Yet",
101                    message: "Workflows save a reusable set of domains so repeat inspections take one tap instead of rebuilding the same batch each time.",
102                    suggestion: "Create a workflow for a weekly audit set, customer domains, or a monitoring group.",
103                    systemImage: "square.stack.3d.down.right",
104                    showsCardBackground: false
105                )
106            }
107            .listRowBackground(Color(.systemGray6).opacity(0.5))
108        } else {
109            ForEach(viewModel.workflows) { workflow in
110                NavigationLink {
111                    WorkflowDetailView(viewModel: viewModel, workflowID: workflow.id)
112                } label: {
113                    WorkflowRowView(workflow: workflow)
114                }
115                .listRowBackground(Color(.systemGray6).opacity(0.5))
116            }
117            .onDelete { offsets in
118                let workflows = offsets.map { viewModel.workflows[$0] }
119                workflows.forEach(viewModel.deleteWorkflow)
120            }
121        }
122    }
123}
124
125private struct WorkflowRowView: View {
126    @Environment(\.appDensity) private var appDensity
127
128    let workflow: DomainWorkflow
129
130    var body: some View {
131        VStack(alignment: .leading, spacing: appDensity.metrics.rowSpacing + 1) {
132            HStack(alignment: .firstTextBaseline, spacing: 8) {
133                if workflow.collaboration?.isShared == true {
134                    Image(systemName: "person.2.fill")
135                        .font(.caption2)
136                        .foregroundStyle(.cyan)
137                }
138                Text(workflow.name)
139                    .font(appDensity.font(.callout, design: .default, weight: .semibold))
140                    .foregroundStyle(.primary)
141                    .lineLimit(2)
142                Spacer(minLength: 8)
143                Text("\(workflow.domains.count) domains")
144                    .font(appDensity.font(.caption2))
145                    .foregroundStyle(.secondary)
146            }
147
148            Text("Updated \(workflow.updatedAt.formatted(date: .abbreviated, time: .shortened))")
149                .font(appDensity.font(.caption2))
150                .foregroundStyle(.secondary)
151
152            if let collaboration = workflow.collaboration, collaboration.isShared {
153                Text("\(collaboration.ownership.title)\(collaboration.permission.title)")
154                    .font(appDensity.font(.caption2))
155                    .foregroundStyle(.secondary)
156            }
157
158            if let notes = workflow.notes, !notes.isEmpty {
159                Text(notes)
160                    .font(appDensity.font(.caption))
161                    .foregroundStyle(.secondary)
162                    .lineLimit(2)
163            }
164        }
165        .frame(maxWidth: .infinity, alignment: .leading)
166        .padding(.vertical, 4)
167    }
168}
169
170struct WorkflowDetailView: View {
171    @Environment(\.appDensity) private var appDensity
172    @Bindable var viewModel: DomainViewModel
173    let workflowID: UUID
174
175    @State private var showingEditor = false
176    @State private var draftDomain = ""
177    @State private var includeAllExports = false
178    @State private var shareEntity: ShareableEntity?
179
180    private var workflow: DomainWorkflow? {
181        viewModel.workflow(withID: workflowID)
182    }
183
184    private var latestSummary: WorkflowRunSummary? {
185        guard viewModel.latestWorkflowRunSummary?.workflowID == workflowID else { return nil }
186        return viewModel.latestWorkflowRunSummary
187    }
188
189    var body: some View {
190        Group {
191            if let workflow {
192                List {
193                    Section("Overview") {
194                        statRow(label: "Domains", value: "\(workflow.domains.count)")
195                        statRow(label: "Created", value: workflow.createdAt.formatted(date: .abbreviated, time: .shortened))
196                        statRow(label: "Updated", value: workflow.updatedAt.formatted(date: .abbreviated, time: .shortened))
197                        if let notes = workflow.notes, !notes.isEmpty {
198                            Text(notes)
199                                .font(appDensity.font(.caption))
200                                .foregroundStyle(.secondary)
201                        }
202                        if let collaboration = workflow.collaboration, collaboration.isShared {
203                            Text("\(collaboration.ownership.title)\(collaboration.permission.title)")
204                                .font(appDensity.font(.caption))
205                                .foregroundStyle(.secondary)
206                        }
207                    }
208
209                    Section("Actions") {
210                        Button {
211                            viewModel.runWorkflow(workflow)
212                        } label: {
213                            Label("Run Workflow", systemImage: "play.fill")
214                        }
215                        .disabled(workflow.domains.isEmpty || viewModel.batchLookupRunning)
216
217                        if viewModel.batchLookupRunning, viewModel.batchLookupSource == .workflow {
218                            Button("Cancel Workflow Run", role: .destructive) {
219                                viewModel.cancelBatchLookup()
220                            }
221                        }
222
223                        Button {
224                            showingEditor = true
225                        } label: {
226                            Label("Edit Workflow", systemImage: "pencil")
227                        }
228                        .disabled(!viewModel.canEdit(workflow))
229
230                        Button {
231                            shareEntity = .workflow(workflow.id)
232                        } label: {
233                            Label(workflow.collaboration?.isShared == true ? "Manage Share" : "Share Workflow", systemImage: "person.2")
234                        }
235
236                        if latestSummary != nil {
237                            Toggle("Export all domains", isOn: $includeAllExports)
238                            Menu {
239                                Button("Export TXT") {
240                                    shareWorkflowResults(format: .text)
241                                }
242                                if FeatureAccessService.hasAccess(to: .advancedExports) {
243                                    Button("Export CSV") {
244                                        shareWorkflowResults(format: .csv)
245                                    }
246                                    Button("Export JSON") {
247                                        shareWorkflowResults(format: .json)
248                                    }
249                                } else {
250                                    Button("CSV Export • Available in Pro") {}
251                                        .disabled(true)
252                                    Button("JSON Export • Available in Pro") {}
253                                        .disabled(true)
254                                }
255                            } label: {
256                                Label("Export Results", systemImage: "square.and.arrow.up")
257                            }
258                        }
259                    }
260
261                    if let latestSummary {
262                        Section("Latest Run") {
263                            statRow(label: "Processed", value: "\(latestSummary.totalDomains)")
264                            statRow(label: "Changed", value: "\(latestSummary.changedDomains)")
265                            statRow(label: "Warnings", value: "\(latestSummary.warningDomains)")
266                            statRow(label: "Unchanged", value: "\(latestSummary.unchangedDomains)")
267                            if !latestSummary.workflowInsights.isEmpty {
268                                Text(latestSummary.workflowInsights[0].description)
269                                    .font(appDensity.font(.caption))
270                                    .foregroundStyle(.secondary)
271                            }
272
273                            Button {
274                                viewModel.latestWorkflowRunSummary = latestSummary
275                            } label: {
276                                Label("View Run Summary", systemImage: "list.bullet.rectangle")
277                            }
278                        }
279                    }
280
281                    Section("Add Domain") {
282                        HStack {
283                            TextField("example.com", text: $draftDomain)
284                                .textInputAutocapitalization(.never)
285                                .autocorrectionDisabled()
286                            Button("Add") {
287                                guard !draftDomain.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return }
288                                viewModel.addDomains([draftDomain], to: workflow)
289                                draftDomain = ""
290                            }
291                            .disabled(!viewModel.canEdit(workflow))
292                        }
293                    }
294
295                    Section("Domains") {
296                        if workflow.domains.isEmpty {
297                            Text("No domains in this workflow")
298                                .font(appDensity.font(.caption))
299                                .foregroundStyle(.secondary)
300                        } else {
301                            ForEach(workflow.domains, id: \.self) { domain in
302                                Button {
303                                    viewModel.openInspection(for: domain)
304                                } label: {
305                                    HStack {
306                                        Text(domain)
307                                            .font(appDensity.font(.callout))
308                                            .foregroundStyle(.primary)
309                                        Spacer()
310                                        Image(systemName: "arrow.up.right.circle")
311                                            .foregroundStyle(.secondary)
312                                    }
313                                }
314                                .buttonStyle(.plain)
315                            }
316                            .onDelete { offsets in
317                                guard viewModel.canEdit(workflow) else { return }
318                                viewModel.removeWorkflowDomains(at: offsets, from: workflow)
319                            }
320                            .onMove { offsets, destination in
321                                guard viewModel.canEdit(workflow) else { return }
322                                viewModel.moveWorkflowDomains(from: offsets, to: destination, in: workflow)
323                            }
324                        }
325                    }
326                }
327                .scrollContentBackground(.hidden)
328                .background(Color.black)
329                .navigationTitle(workflow.name)
330                .toolbar {
331                    if !workflow.domains.isEmpty {
332                        EditButton()
333                            .disabled(!viewModel.canEdit(workflow))
334                    }
335                }
336                .sheet(isPresented: $showingEditor) {
337                    WorkflowComposerView(viewModel: viewModel, workflow: workflow)
338                }
339                .sheet(item: workflowSummaryBinding) { summary in
340                    WorkflowRunSummaryView(viewModel: viewModel, summary: summary)
341                }
342                .sheet(item: $shareEntity) { entity in
343                    CloudSharingSheet(entity: entity, title: workflow.name)
344                }
345            } else {
346                Text("Workflow not found")
347                    .font(appDensity.font(.callout))
348                    .foregroundStyle(.secondary)
349            }
350        }
351        .preferredColorScheme(.dark)
352    }
353
354    private func statRow(label: String, value: String) -> some View {
355        HStack {
356            Text(label)
357                .font(appDensity.font(.caption))
358                .foregroundStyle(.secondary)
359            Spacer()
360            Text(value)
361                .font(appDensity.font(.callout))
362                .foregroundStyle(.primary)
363        }
364    }
365
366    private var workflowSummaryBinding: Binding<WorkflowRunSummary?> {
367        Binding(
368            get: {
369                guard viewModel.latestWorkflowRunSummary?.workflowID == workflowID else { return nil }
370                return viewModel.latestWorkflowRunSummary
371            },
372            set: { _ in
373                viewModel.latestWorkflowRunSummary = nil
374            }
375        )
376    }
377
378    private func shareWorkflowResults(format: DomainExportFormat) {
379        guard let latestSummary else { return }
380
381        let changedOnly = !includeAllExports
382        let formatter = DateFormatter()
383        formatter.dateFormat = "yyyyMMdd_HHmmss"
384        let timestamp = formatter.string(from: Date())
385        let filename = "\(timestamp)_workflow_\(latestSummary.workflowName.replacingOccurrences(of: " ", with: "_").lowercased()).\(format.fileExtension)"
386        let data: Data
387
388        switch format {
389        case .text:
390            data = Data(viewModel.exportWorkflowText(summary: latestSummary, changedOnly: changedOnly).utf8)
391        case .csv:
392            data = Data(viewModel.exportWorkflowCSV(summary: latestSummary, changedOnly: changedOnly).utf8)
393        case .json:
394            data = viewModel.exportWorkflowJSONData(summary: latestSummary, changedOnly: changedOnly) ?? Data("[]".utf8)
395        }
396
397        ExportPresenter.share(filename: filename, data: data)
398    }
399}
400
401struct WorkflowComposerView: View {
402    @Environment(\.dismiss) private var dismiss
403    @Bindable var viewModel: DomainViewModel
404    let workflow: DomainWorkflow?
405
406    @State private var name = ""
407    @State private var domainsText = ""
408    @State private var notes = ""
409
410    init(viewModel: DomainViewModel, workflow: DomainWorkflow? = nil) {
411        self.viewModel = viewModel
412        self.workflow = workflow
413        _name = State(initialValue: workflow?.name ?? "")
414        _domainsText = State(initialValue: workflow?.domains.joined(separator: "\n") ?? "")
415        _notes = State(initialValue: workflow?.notes ?? "")
416    }
417
418    var body: some View {
419        NavigationStack {
420            Form {
421                Section("Workflow") {
422                    TextField("Name", text: $name)
423                    TextField("Notes", text: $notes, axis: .vertical)
424                        .lineLimit(3...6)
425                }
426
427                Section("Domains") {
428                    TextField("example.com\napple.com", text: $domainsText, axis: .vertical)
429                        .textInputAutocapitalization(.never)
430                        .autocorrectionDisabled()
431                        .lineLimit(6...14)
432                }
433            }
434            .navigationTitle(workflow == nil ? "New Workflow" : "Edit Workflow")
435            .toolbar {
436                ToolbarItem(placement: .cancellationAction) {
437                    Button("Cancel") {
438                        dismiss()
439                    }
440                }
441                ToolbarItem(placement: .confirmationAction) {
442                    Button("Save") {
443                        saveWorkflow()
444                    }
445                    .disabled(name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || parsedDomains.isEmpty)
446                }
447            }
448        }
449        .preferredColorScheme(.dark)
450    }
451
452    private var parsedDomains: [String] {
453        domainsText
454            .components(separatedBy: CharacterSet(charactersIn: ",\n"))
455            .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
456            .filter { !$0.isEmpty }
457    }
458
459    private func saveWorkflow() {
460        if let workflow {
461            viewModel.updateWorkflow(workflow, name: name, domains: parsedDomains, notes: notes)
462            dismiss()
463            return
464        }
465
466        if viewModel.createWorkflow(name: name, domains: parsedDomains, notes: notes) != nil {
467            dismiss()
468        }
469    }
470}
471
472struct WorkflowRunSummaryView: View {
473    @Bindable var viewModel: DomainViewModel
474    let summary: WorkflowRunSummary
475
476    @State private var showAllResults = false
477
478    private var visibleResults: [BatchLookupResult] {
479        showAllResults ? summary.results : summary.results.filter(\.hasMeaningfulChange)
480    }
481
482    var body: some View {
483        NavigationStack {
484            List {
485                Section("Overview") {
486                    statRow(label: "Workflow", value: summary.workflowName)
487                    statRow(label: "Processed", value: "\(summary.totalDomains)")
488                    statRow(label: "Changed", value: "\(summary.changedDomains)")
489                    statRow(label: "Warnings", value: "\(summary.warningDomains)")
490                    statRow(label: "Unchanged", value: "\(summary.unchangedDomains)")
491                    statRow(label: "Finished", value: summary.generatedAt.formatted(date: .abbreviated, time: .shortened))
492                }
493
494                Section {
495                    Toggle("Show unchanged domains", isOn: $showAllResults)
496                }
497
498                if !summary.workflowInsights.isEmpty {
499                    Section("Workflow Insights") {
500                        ForEach(summary.workflowInsights) { insight in
501                            VStack(alignment: .leading, spacing: 4) {
502                                Text(insight.description)
503                                    .font(.system(.callout, design: .monospaced))
504                                    .foregroundStyle(.primary)
505                                Text(insight.domainsInvolved.joined(separator: ", "))
506                                    .font(.system(.caption, design: .monospaced))
507                                    .foregroundStyle(.secondary)
508                            }
509                        }
510                    }
511                }
512
513                Section(visibleResults.isEmpty ? "Meaningful Changes" : "Results") {
514                    if visibleResults.isEmpty {
515                        Text("No domains with meaningful changes or warnings")
516                            .font(.system(.caption, design: .monospaced))
517                            .foregroundStyle(.secondary)
518                    } else {
519                        ForEach(visibleResults) { result in
520                            if let entry = viewModel.historyEntry(for: result) {
521                                NavigationLink {
522                                    HistoryDetailView(viewModel: viewModel, entry: entry)
523                                } label: {
524                                    BatchResultRowView(result: result)
525                                }
526                            } else {
527                                BatchResultRowView(result: result)
528                            }
529                        }
530                    }
531                }
532            }
533            .navigationTitle("Workflow Summary")
534            .toolbar {
535                ToolbarItemGroup(placement: .topBarTrailing) {
536                    Menu {
537                        Button("Export TXT") {
538                            share(format: .text)
539                        }
540                        if FeatureAccessService.hasAccess(to: .advancedExports) {
541                            Button("Export CSV") {
542                                share(format: .csv)
543                            }
544                            Button("Export JSON") {
545                                share(format: .json)
546                            }
547                        } else {
548                            Button("CSV Export • Available in Pro") {}
549                                .disabled(true)
550                            Button("JSON Export • Available in Pro") {}
551                                .disabled(true)
552                        }
553                    } label: {
554                        Image(systemName: "square.and.arrow.up")
555                    }
556
557                    if let workflow = viewModel.workflow(withID: summary.workflowID) {
558                        Button {
559                            viewModel.runWorkflow(workflow)
560                        } label: {
561                            Image(systemName: "arrow.clockwise")
562                        }
563                        .disabled(viewModel.batchLookupRunning)
564                    }
565                }
566            }
567        }
568        .preferredColorScheme(.dark)
569    }
570
571    private func statRow(label: String, value: String) -> some View {
572        HStack {
573            Text(label)
574                .font(.system(.caption, design: .monospaced))
575                .foregroundStyle(.secondary)
576            Spacer()
577            Text(value)
578                .font(.system(.callout, design: .monospaced))
579                .foregroundStyle(.primary)
580        }
581    }
582
583    private func share(format: DomainExportFormat) {
584        let changedOnly = !showAllResults
585        let formatter = DateFormatter()
586        formatter.dateFormat = "yyyyMMdd_HHmmss"
587        let timestamp = formatter.string(from: Date())
588        let filename = "\(timestamp)_workflow_summary.\(format.fileExtension)"
589        let data: Data
590
591        switch format {
592        case .text:
593            data = Data(viewModel.exportWorkflowText(summary: summary, changedOnly: changedOnly).utf8)
594        case .csv:
595            data = Data(viewModel.exportWorkflowCSV(summary: summary, changedOnly: changedOnly).utf8)
596        case .json:
597            data = viewModel.exportWorkflowJSONData(summary: summary, changedOnly: changedOnly) ?? Data("[]".utf8)
598        }
599
600        ExportPresenter.share(filename: filename, data: data)
601    }
602}
603
604struct WorkflowBulkAddSheet: View {
605    @Environment(\.dismiss) private var dismiss
606    @Bindable var viewModel: DomainViewModel
607    let title: String
608    let availableDomains: [String]
609
610    @State private var mode: WorkflowDestinationMode = .existing
611    @State private var selectedDomains: Set<String>
612    @State private var selectedWorkflowID: UUID?
613    @State private var newWorkflowName = ""
614    @State private var newWorkflowNotes = ""
615
616    init(viewModel: DomainViewModel, title: String, availableDomains: [String]) {
617        self.viewModel = viewModel
618        self.title = title
619        self.availableDomains = Array(Set(availableDomains)).sorted()
620        _selectedDomains = State(initialValue: Set(availableDomains))
621        _selectedWorkflowID = State(initialValue: viewModel.workflows.first?.id)
622    }
623
624    var body: some View {
625        NavigationStack {
626            List {
627                Section("Selection") {
628                    ForEach(availableDomains, id: \.self) { domain in
629                        Button {
630                            toggle(domain)
631                        } label: {
632                            HStack {
633                                Text(domain)
634                                    .foregroundStyle(.primary)
635                                Spacer()
636                                Image(systemName: selectedDomains.contains(domain) ? "checkmark.circle.fill" : "circle")
637                                    .foregroundStyle(selectedDomains.contains(domain) ? .cyan : .secondary)
638                            }
639                        }
640                        .buttonStyle(.plain)
641                    }
642                }
643
644                Section("Destination") {
645                    Picker("Destination", selection: $mode) {
646                        Text("Existing").tag(WorkflowDestinationMode.existing)
647                        Text("New").tag(WorkflowDestinationMode.new)
648                    }
649                    .pickerStyle(.segmented)
650
651                    if mode == .existing, !viewModel.workflows.isEmpty {
652                        Picker("Workflow", selection: $selectedWorkflowID) {
653                            ForEach(viewModel.workflows) { workflow in
654                                Text(workflow.name).tag(Optional(workflow.id))
655                            }
656                        }
657                    } else {
658                        TextField("Workflow name", text: $newWorkflowName)
659                        TextField("Notes", text: $newWorkflowNotes, axis: .vertical)
660                            .lineLimit(2...5)
661                    }
662                }
663            }
664            .navigationTitle(title)
665            .toolbar {
666                ToolbarItem(placement: .cancellationAction) {
667                    Button("Cancel") {
668                        dismiss()
669                    }
670                }
671                ToolbarItem(placement: .confirmationAction) {
672                    Button("Save") {
673                        save()
674                    }
675                    .disabled(!canSave)
676                }
677            }
678            .onAppear {
679                if viewModel.workflows.isEmpty {
680                    mode = .new
681                }
682            }
683        }
684        .preferredColorScheme(.dark)
685    }
686
687    private var selectedDomainList: [String] {
688        availableDomains.filter { selectedDomains.contains($0) }
689    }
690
691    private var canSave: Bool {
692        guard !selectedDomainList.isEmpty else { return false }
693        switch mode {
694        case .existing:
695            return selectedWorkflowID != nil
696        case .new:
697            return !newWorkflowName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
698        }
699    }
700
701    private func toggle(_ domain: String) {
702        if selectedDomains.contains(domain) {
703            selectedDomains.remove(domain)
704        } else {
705            selectedDomains.insert(domain)
706        }
707    }
708
709    private func save() {
710        switch mode {
711        case .existing:
712            guard let selectedWorkflowID, let workflow = viewModel.workflow(withID: selectedWorkflowID) else { return }
713            viewModel.addDomains(selectedDomainList, to: workflow)
714            dismiss()
715        case .new:
716            guard viewModel.createWorkflow(name: newWorkflowName, domains: selectedDomainList, notes: newWorkflowNotes) != nil else {
717                return
718            }
719            dismiss()
720        }
721    }
722}