krz/domain-dig

an ios app for DNS & SSL analysis

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

v4.8.2: 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                                    Button("Export Markdown") {
250                                        shareWorkflowResults(format: .markdown)
251                                    }
252                                    Button("Export PDF") {
253                                        shareWorkflowResults(format: .pdf)
254                                    }
255                                } else {
256                                    Button("CSV Export • Available in Pro") {}
257                                        .disabled(true)
258                                    Button("JSON Export • Available in Pro") {}
259                                        .disabled(true)
260                                    Button("Markdown Export • Available in Pro") {}
261                                        .disabled(true)
262                                    Button("PDF Export • Available in Pro") {}
263                                        .disabled(true)
264                                }
265                            } label: {
266                                Label("Export Results", systemImage: "square.and.arrow.up")
267                            }
268                        }
269                    }
270
271                    if let latestSummary {
272                        Section("Latest Run") {
273                            statRow(label: "Processed", value: "\(latestSummary.totalDomains)")
274                            statRow(label: "Changed", value: "\(latestSummary.changedDomains)")
275                            statRow(label: "Warnings", value: "\(latestSummary.warningDomains)")
276                            statRow(label: "Unchanged", value: "\(latestSummary.unchangedDomains)")
277                            if !latestSummary.workflowInsights.isEmpty {
278                                Text(latestSummary.workflowInsights[0].description)
279                                    .font(appDensity.font(.caption))
280                                    .foregroundStyle(.secondary)
281                            }
282
283                            Button {
284                                viewModel.latestWorkflowRunSummary = latestSummary
285                            } label: {
286                                Label("View Run Summary", systemImage: "list.bullet.rectangle")
287                            }
288                        }
289                    }
290
291                    Section("Add Domain") {
292                        HStack {
293                            TextField("example.com", text: $draftDomain)
294                                .textInputAutocapitalization(.never)
295                                .autocorrectionDisabled()
296                            Button("Add") {
297                                guard !draftDomain.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return }
298                                viewModel.addDomains([draftDomain], to: workflow)
299                                draftDomain = ""
300                            }
301                            .disabled(!viewModel.canEdit(workflow))
302                        }
303                    }
304
305                    Section("Domains") {
306                        if workflow.domains.isEmpty {
307                            Text("No domains in this workflow")
308                                .font(appDensity.font(.caption))
309                                .foregroundStyle(.secondary)
310                        } else {
311                            ForEach(workflow.domains, id: \.self) { domain in
312                                Button {
313                                    viewModel.openInspection(for: domain)
314                                } label: {
315                                    HStack {
316                                        Text(domain)
317                                            .font(appDensity.font(.callout))
318                                            .foregroundStyle(.primary)
319                                        Spacer()
320                                        Image(systemName: "arrow.up.right.circle")
321                                            .foregroundStyle(.secondary)
322                                    }
323                                }
324                                .buttonStyle(.plain)
325                            }
326                            .onDelete { offsets in
327                                guard viewModel.canEdit(workflow) else { return }
328                                viewModel.removeWorkflowDomains(at: offsets, from: workflow)
329                            }
330                            .onMove { offsets, destination in
331                                guard viewModel.canEdit(workflow) else { return }
332                                viewModel.moveWorkflowDomains(from: offsets, to: destination, in: workflow)
333                            }
334                        }
335                    }
336                }
337                .scrollContentBackground(.hidden)
338                .background(Color.black)
339                .navigationTitle(workflow.name)
340                .toolbar {
341                    if !workflow.domains.isEmpty {
342                        EditButton()
343                            .disabled(!viewModel.canEdit(workflow))
344                    }
345                }
346                .sheet(isPresented: $showingEditor) {
347                    WorkflowComposerView(viewModel: viewModel, workflow: workflow)
348                }
349                .sheet(item: workflowSummaryBinding) { summary in
350                    WorkflowRunSummaryView(viewModel: viewModel, summary: summary)
351                }
352                .sheet(item: $shareEntity) { entity in
353                    CloudSharingSheet(entity: entity, title: workflow.name)
354                }
355            } else {
356                Text("Workflow not found")
357                    .font(appDensity.font(.callout))
358                    .foregroundStyle(.secondary)
359            }
360        }
361        .preferredColorScheme(.dark)
362    }
363
364    private func statRow(label: String, value: String) -> some View {
365        HStack {
366            Text(label)
367                .font(appDensity.font(.caption))
368                .foregroundStyle(.secondary)
369            Spacer()
370            Text(value)
371                .font(appDensity.font(.callout))
372                .foregroundStyle(.primary)
373        }
374    }
375
376    private var workflowSummaryBinding: Binding<WorkflowRunSummary?> {
377        Binding(
378            get: {
379                guard viewModel.latestWorkflowRunSummary?.workflowID == workflowID else { return nil }
380                return viewModel.latestWorkflowRunSummary
381            },
382            set: { _ in
383                viewModel.latestWorkflowRunSummary = nil
384            }
385        )
386    }
387
388    private func shareWorkflowResults(format: DomainExportFormat) {
389        guard let latestSummary else { return }
390
391        let changedOnly = !includeAllExports
392        let formatter = DateFormatter()
393        formatter.dateFormat = "yyyyMMdd_HHmmss"
394        let timestamp = formatter.string(from: Date())
395        let filename = "\(timestamp)_workflow_\(latestSummary.workflowName.replacingOccurrences(of: " ", with: "_").lowercased()).\(format.fileExtension)"
396        guard let data = viewModel.exportWorkflowData(summary: latestSummary, changedOnly: changedOnly, format: format) else {
397            return
398        }
399
400        ExportPresenter.share(filename: filename, data: data)
401    }
402}
403
404struct WorkflowComposerView: View {
405    @Environment(\.dismiss) private var dismiss
406    @Bindable var viewModel: DomainViewModel
407    let workflow: DomainWorkflow?
408
409    @State private var name = ""
410    @State private var domainsText = ""
411    @State private var notes = ""
412
413    init(viewModel: DomainViewModel, workflow: DomainWorkflow? = nil) {
414        self.viewModel = viewModel
415        self.workflow = workflow
416        _name = State(initialValue: workflow?.name ?? "")
417        _domainsText = State(initialValue: workflow?.domains.joined(separator: "\n") ?? "")
418        _notes = State(initialValue: workflow?.notes ?? "")
419    }
420
421    var body: some View {
422        NavigationStack {
423            Form {
424                Section("Workflow") {
425                    TextField("Name", text: $name)
426                    TextField("Notes", text: $notes, axis: .vertical)
427                        .lineLimit(3...6)
428                }
429
430                Section("Domains") {
431                    TextField("example.com\napple.com", text: $domainsText, axis: .vertical)
432                        .textInputAutocapitalization(.never)
433                        .autocorrectionDisabled()
434                        .lineLimit(6...14)
435                }
436            }
437            .navigationTitle(workflow == nil ? "New Workflow" : "Edit Workflow")
438            .toolbar {
439                ToolbarItem(placement: .cancellationAction) {
440                    Button("Cancel") {
441                        dismiss()
442                    }
443                }
444                ToolbarItem(placement: .confirmationAction) {
445                    Button("Save") {
446                        saveWorkflow()
447                    }
448                    .disabled(name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || parsedDomains.isEmpty)
449                }
450            }
451        }
452        .preferredColorScheme(.dark)
453    }
454
455    private var parsedDomains: [String] {
456        domainsText
457            .components(separatedBy: CharacterSet(charactersIn: ",\n"))
458            .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
459            .filter { !$0.isEmpty }
460    }
461
462    private func saveWorkflow() {
463        if let workflow {
464            viewModel.updateWorkflow(workflow, name: name, domains: parsedDomains, notes: notes)
465            dismiss()
466            return
467        }
468
469        if viewModel.createWorkflow(name: name, domains: parsedDomains, notes: notes) != nil {
470            dismiss()
471        }
472    }
473}
474
475struct WorkflowRunSummaryView: View {
476    @Bindable var viewModel: DomainViewModel
477    let summary: WorkflowRunSummary
478
479    @State private var showAllResults = false
480
481    private var visibleResults: [BatchLookupResult] {
482        showAllResults ? summary.results : summary.results.filter(\.hasMeaningfulChange)
483    }
484
485    var body: some View {
486        NavigationStack {
487            List {
488                Section("Overview") {
489                    statRow(label: "Workflow", value: summary.workflowName)
490                    statRow(label: "Processed", value: "\(summary.totalDomains)")
491                    statRow(label: "Changed", value: "\(summary.changedDomains)")
492                    statRow(label: "Warnings", value: "\(summary.warningDomains)")
493                    statRow(label: "Unchanged", value: "\(summary.unchangedDomains)")
494                    statRow(label: "Finished", value: summary.generatedAt.formatted(date: .abbreviated, time: .shortened))
495                }
496
497                Section {
498                    Toggle("Show unchanged domains", isOn: $showAllResults)
499                }
500
501                if !summary.workflowInsights.isEmpty {
502                    Section("Workflow Insights") {
503                        ForEach(summary.workflowInsights) { insight in
504                            VStack(alignment: .leading, spacing: 4) {
505                                Text(insight.description)
506                                    .font(.system(.callout, design: .monospaced))
507                                    .foregroundStyle(.primary)
508                                Text(insight.domainsInvolved.joined(separator: ", "))
509                                    .font(.system(.caption, design: .monospaced))
510                                    .foregroundStyle(.secondary)
511                            }
512                        }
513                    }
514                }
515
516                Section(visibleResults.isEmpty ? "Meaningful Changes" : "Results") {
517                    if visibleResults.isEmpty {
518                        Text("No domains with meaningful changes or warnings")
519                            .font(.system(.caption, design: .monospaced))
520                            .foregroundStyle(.secondary)
521                    } else {
522                        ForEach(visibleResults) { result in
523                            if let entry = viewModel.historyEntry(for: result) {
524                                NavigationLink {
525                                    HistoryDetailView(viewModel: viewModel, entry: entry)
526                                } label: {
527                                    BatchResultRowView(result: result)
528                                }
529                            } else {
530                                BatchResultRowView(result: result)
531                            }
532                        }
533                    }
534                }
535            }
536            .navigationTitle("Workflow Summary")
537            .toolbar {
538                ToolbarItemGroup(placement: .topBarTrailing) {
539                    Menu {
540                        Button("Export TXT") {
541                            share(format: .text)
542                        }
543                        if FeatureAccessService.hasAccess(to: .advancedExports) {
544                            Button("Export CSV") {
545                                share(format: .csv)
546                            }
547                            Button("Export JSON") {
548                                share(format: .json)
549                            }
550                            Button("Export Markdown") {
551                                share(format: .markdown)
552                            }
553                            Button("Export PDF") {
554                                share(format: .pdf)
555                            }
556                        } else {
557                            Button("CSV Export • Available in Pro") {}
558                                .disabled(true)
559                            Button("JSON Export • Available in Pro") {}
560                                .disabled(true)
561                            Button("Markdown Export • Available in Pro") {}
562                                .disabled(true)
563                            Button("PDF Export • Available in Pro") {}
564                                .disabled(true)
565                        }
566                    } label: {
567                        Image(systemName: "square.and.arrow.up")
568                    }
569
570                    if let workflow = viewModel.workflow(withID: summary.workflowID) {
571                        Button {
572                            viewModel.runWorkflow(workflow)
573                        } label: {
574                            Image(systemName: "arrow.clockwise")
575                        }
576                        .disabled(viewModel.batchLookupRunning)
577                    }
578                }
579            }
580        }
581        .preferredColorScheme(.dark)
582    }
583
584    private func statRow(label: String, value: String) -> some View {
585        HStack {
586            Text(label)
587                .font(.system(.caption, design: .monospaced))
588                .foregroundStyle(.secondary)
589            Spacer()
590            Text(value)
591                .font(.system(.callout, design: .monospaced))
592                .foregroundStyle(.primary)
593        }
594    }
595
596    private func share(format: DomainExportFormat) {
597        let changedOnly = !showAllResults
598        let formatter = DateFormatter()
599        formatter.dateFormat = "yyyyMMdd_HHmmss"
600        let timestamp = formatter.string(from: Date())
601        let filename = "\(timestamp)_workflow_summary.\(format.fileExtension)"
602        guard let data = viewModel.exportWorkflowData(summary: summary, changedOnly: changedOnly, format: format) else {
603            return
604        }
605
606        ExportPresenter.share(filename: filename, data: data)
607    }
608}
609
610struct WorkflowBulkAddSheet: View {
611    @Environment(\.dismiss) private var dismiss
612    @Bindable var viewModel: DomainViewModel
613    let title: String
614    let availableDomains: [String]
615
616    @State private var mode: WorkflowDestinationMode = .existing
617    @State private var selectedDomains: Set<String>
618    @State private var selectedWorkflowID: UUID?
619    @State private var newWorkflowName = ""
620    @State private var newWorkflowNotes = ""
621
622    init(viewModel: DomainViewModel, title: String, availableDomains: [String]) {
623        self.viewModel = viewModel
624        self.title = title
625        self.availableDomains = Array(Set(availableDomains)).sorted()
626        _selectedDomains = State(initialValue: Set(availableDomains))
627        _selectedWorkflowID = State(initialValue: viewModel.workflows.first?.id)
628    }
629
630    var body: some View {
631        NavigationStack {
632            List {
633                Section("Selection") {
634                    ForEach(availableDomains, id: \.self) { domain in
635                        Button {
636                            toggle(domain)
637                        } label: {
638                            HStack {
639                                Text(domain)
640                                    .foregroundStyle(.primary)
641                                Spacer()
642                                Image(systemName: selectedDomains.contains(domain) ? "checkmark.circle.fill" : "circle")
643                                    .foregroundStyle(selectedDomains.contains(domain) ? .cyan : .secondary)
644                            }
645                        }
646                        .buttonStyle(.plain)
647                    }
648                }
649
650                Section("Destination") {
651                    Picker("Destination", selection: $mode) {
652                        Text("Existing").tag(WorkflowDestinationMode.existing)
653                        Text("New").tag(WorkflowDestinationMode.new)
654                    }
655                    .pickerStyle(.segmented)
656
657                    if mode == .existing, !viewModel.workflows.isEmpty {
658                        Picker("Workflow", selection: $selectedWorkflowID) {
659                            ForEach(viewModel.workflows) { workflow in
660                                Text(workflow.name).tag(Optional(workflow.id))
661                            }
662                        }
663                    } else {
664                        TextField("Workflow name", text: $newWorkflowName)
665                        TextField("Notes", text: $newWorkflowNotes, axis: .vertical)
666                            .lineLimit(2...5)
667                    }
668                }
669            }
670            .navigationTitle(title)
671            .toolbar {
672                ToolbarItem(placement: .cancellationAction) {
673                    Button("Cancel") {
674                        dismiss()
675                    }
676                }
677                ToolbarItem(placement: .confirmationAction) {
678                    Button("Save") {
679                        save()
680                    }
681                    .disabled(!canSave)
682                }
683            }
684            .onAppear {
685                if viewModel.workflows.isEmpty {
686                    mode = .new
687                }
688            }
689        }
690        .preferredColorScheme(.dark)
691    }
692
693    private var selectedDomainList: [String] {
694        availableDomains.filter { selectedDomains.contains($0) }
695    }
696
697    private var canSave: Bool {
698        guard !selectedDomainList.isEmpty else { return false }
699        switch mode {
700        case .existing:
701            return selectedWorkflowID != nil
702        case .new:
703            return !newWorkflowName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
704        }
705    }
706
707    private func toggle(_ domain: String) {
708        if selectedDomains.contains(domain) {
709            selectedDomains.remove(domain)
710        } else {
711            selectedDomains.insert(domain)
712        }
713    }
714
715    private func save() {
716        switch mode {
717        case .existing:
718            guard let selectedWorkflowID, let workflow = viewModel.workflow(withID: selectedWorkflowID) else { return }
719            viewModel.addDomains(selectedDomainList, to: workflow)
720            dismiss()
721        case .new:
722            guard viewModel.createWorkflow(name: newWorkflowName, domains: selectedDomainList, notes: newWorkflowNotes) != nil else {
723                return
724            }
725            dismiss()
726        }
727    }
728}