From 9384a668bf36f23f71e25d05b490d1040d308845 Mon Sep 17 00:00:00 2001 From: Christian Cleberg Date: Wed, 22 Apr 2026 10:14:30 -0500 Subject: feat(v2.8.0): add workflows and repeatable inspection automation * introduce DomainWorkflow model and persistence * add WorkflowsView and execution flow * support batch inspection via workflows * add workflow-based export (TXT/CSV/JSON) * enable bulk add from history/watchlist * add quick actions for common operations * integrate workflows with tracking and history --- DomainDig/ContentView.swift | 112 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) (limited to 'DomainDig/ContentView.swift') diff --git a/DomainDig/ContentView.swift b/DomainDig/ContentView.swift index ed2da49..b023547 100644 --- a/DomainDig/ContentView.swift +++ b/DomainDig/ContentView.swift @@ -18,6 +18,10 @@ enum ResultSection: String, Hashable { case subdomains } +private struct WorkflowNavigationTarget: Hashable { + let workflowID: UUID +} + struct ContentView: View { @Environment(\.appDensity) private var appDensity @State private var viewModel = DomainViewModel() @@ -30,6 +34,8 @@ struct ContentView: View { @State private var showTrackLimitAlert = false @State private var inputMode: LookupInputMode = .single @State private var collapsedSections: Set = [.network] + @State private var showingCurrentDomainWorkflowSheet = false + @State private var showingBatchWorkflowSheet = false var body: some View { NavigationStack(path: $navigationPath) { @@ -68,6 +74,7 @@ struct ContentView: View { confidence: viewModel.currentSnapshot.availabilityConfidence, snapshotNote: viewModel.currentSnapshot.note, trackedDomain: viewModel.currentTrackedDomain, + workflows: viewModel.currentDomainWorkflows, trackingLimitMessage: viewModel.trackingLimitMessage, onTrack: { if !viewModel.trackCurrentDomain() { @@ -82,6 +89,15 @@ struct ContentView: View { guard let trackedDomain = viewModel.currentTrackedDomain else { return } trackingNoteDraft = trackedDomain.note ?? "" editingTrackedDomain = trackedDomain + }, + onAddToWorkflow: { + showingCurrentDomainWorkflowSheet = true + }, + onOpenWorkflow: { workflow in + navigationPath.append(WorkflowNavigationTarget(workflowID: workflow.id)) + }, + onRunWorkflow: { workflow in + viewModel.rerunCurrentDomain(in: workflow) } ) .padding(.top, appDensity.metrics.sectionSpacing) @@ -224,6 +240,12 @@ struct ContentView: View { Label("Saved Domains", systemImage: "bookmark") } + NavigationLink { + WorkflowsView(viewModel: viewModel) + } label: { + Label("Workflows", systemImage: "square.stack.3d.down.right") + } + NavigationLink { SettingsView(viewModel: viewModel) } label: { @@ -235,6 +257,9 @@ struct ContentView: View { } } } + .navigationDestination(for: WorkflowNavigationTarget.self) { target in + WorkflowDetailView(viewModel: viewModel, workflowID: target.workflowID) + } } .onAppear { domainFieldFocused = true @@ -277,6 +302,20 @@ struct ContentView: View { } } } + .sheet(isPresented: $showingCurrentDomainWorkflowSheet) { + WorkflowBulkAddSheet( + viewModel: viewModel, + title: "Add Domain to Workflow", + availableDomains: [viewModel.searchedDomain] + ) + } + .sheet(isPresented: $showingBatchWorkflowSheet) { + WorkflowBulkAddSheet( + viewModel: viewModel, + title: "Add Batch Domains", + availableDomains: viewModel.batchResults.map(\.domain) + ) + } } private var inputSection: some View { @@ -353,6 +392,30 @@ struct ContentView: View { HStack { Spacer() if viewModel.resultsLoaded { + Menu { + if !viewModel.isCurrentDomainTracked { + Button("Track this domain") { + if !viewModel.trackCurrentDomain() { + showTrackLimitAlert = true + } + } + } + Button("Add to workflow") { + showingCurrentDomainWorkflowSheet = true + } + Button("Copy report JSON") { + guard let json = viewModel.exportJSONString() else { return } + AppClipboard.copy(json) + AppHaptics.copy() + } + Button("Export report") { + shareSingleResults(format: .json) + } + } label: { + Image(systemName: "bolt.circle") + .font(appDensity.font(.body, design: .default)) + .foregroundStyle(.secondary) + } Button { viewModel.toggleSavedDomain() } label: { @@ -392,6 +455,10 @@ struct ContentView: View { } if !viewModel.currentBatchResultEntries.isEmpty { Menu { + Button("Add to Workflow") { + showingBatchWorkflowSheet = true + } + Divider() Button("Export Batch TXT") { shareBatchResults(format: .text) } @@ -982,10 +1049,14 @@ struct DomainSectionView: View { let confidence: ConfidenceLevel? let snapshotNote: String? let trackedDomain: TrackedDomain? + let workflows: [DomainWorkflow] let trackingLimitMessage: String? let onTrack: () -> Void let onTogglePinned: () -> Void let onEditNote: (() -> Void)? + let onAddToWorkflow: (() -> Void)? + let onOpenWorkflow: ((DomainWorkflow) -> Void)? + let onRunWorkflow: ((DomainWorkflow) -> Void)? var body: some View { CollapsibleSectionView(title: "Domain", isCollapsed: $isCollapsed) { @@ -1032,6 +1103,47 @@ struct DomainSectionView: View { MessageRowView(text: trackingLimitMessage, isError: false) .padding(.top, 4) } + if let onAddToWorkflow { + Button { + onAddToWorkflow() + } label: { + Label("Add to workflow", systemImage: "plus.rectangle.on.folder") + .font(appDensity.font(.caption)) + } + .buttonStyle(.bordered) + .padding(.top, 4) + } + if !workflows.isEmpty { + VStack(alignment: .leading, spacing: 8) { + Text("Part of workflow") + .font(appDensity.font(.caption)) + .foregroundStyle(.secondary) + + ForEach(workflows) { workflow in + HStack { + Text(workflow.name) + .font(appDensity.font(.caption)) + .foregroundStyle(.primary) + Spacer() + if let onOpenWorkflow { + Button("Open") { + onOpenWorkflow(workflow) + } + .buttonStyle(.bordered) + .font(appDensity.font(.caption2)) + } + if let onRunWorkflow { + Button("Run") { + onRunWorkflow(workflow) + } + .buttonStyle(.bordered) + .font(appDensity.font(.caption2)) + } + } + } + } + .padding(.top, 4) + } if availabilityLoading { ProgressView("Checking availability…") .appLoadingStyle() -- cgit v1.2.3