summaryrefslogtreecommitdiff
path: root/DomainDig
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-07-25 00:05:53 -0500
committerChristian Cleberg <[email protected]>2026-07-25 00:07:52 -0500
commit9092ab60f82152cc4ae38677c3af4a76013f4e54 (patch)
tree42f787080cf46d8cb906ec7b854bb802223be5eb /DomainDig
parente5d6c6c30df78e1d9b07088c256421aa663cb173 (diff)
downloaddomain-dig-9092ab60f82152cc4ae38677c3af4a76013f4e54.tar.gz
domain-dig-9092ab60f82152cc4ae38677c3af4a76013f4e54.tar.bz2
domain-dig-9092ab60f82152cc4ae38677c3af4a76013f4e54.zip
refactor: extract workflow surface into DomainViewModel+Workflows (v5 step 4, 4/n)
Fourth slice of the DomainViewModel decomposition (off main; the audit/ monitoring/export stack has merged). - Moves the workflow surface into DomainViewModel+Workflows.swift: workflow lookup (workflow(withID:)/workflowsContaining), the DomainWorkflow collaboration checks, the CRUD mutators (create/update/delete/add/remove/move), runWorkflow/rerunCurrentDomain, and refreshWorkflowList. Pure move. The TrackedDomain overloads of canEdit/canDelete/collaborationLabel stay on the main type (they're watchlist collaboration, not workflow). - Promotes the shared helpers the moved methods reach to internal, all staying on the main type: persistWorkflows (also called by clearWorkflows), startBatchLookup (the batch primitive shared with manual/watchlist runs), normalizedDomain/normalizedDomains, loadWorkflows, and the activeWorkflowRunID/ Name state. The extension carries its own fileprivate String.nilIfEmpty, matching the per-file pattern already used across the codebase. DomainViewModel.swift: 4433 -> 4306 lines (4864 at the start of step 4). App builds clean; unit suite 58/58. No project.pbxproj change.
Diffstat (limited to 'DomainDig')
-rw-r--r--DomainDig/DomainViewModel+Workflows.swift144
-rw-r--r--DomainDig/DomainViewModel.swift141
2 files changed, 151 insertions, 134 deletions
diff --git a/DomainDig/DomainViewModel+Workflows.swift b/DomainDig/DomainViewModel+Workflows.swift
new file mode 100644
index 0000000..d1e69b4
--- /dev/null
+++ b/DomainDig/DomainViewModel+Workflows.swift
@@ -0,0 +1,144 @@
+import Foundation
+import SwiftUI
+
+/// Workflow surface of `DomainViewModel`: looking up workflows, the
+/// collaboration permission checks for a workflow, the CRUD mutators, and
+/// running a workflow's domains as a batch.
+///
+/// `runWorkflow`/`rerunCurrentDomain` drive `startBatchLookup` (the shared batch
+/// primitive used by manual and watchlist runs too), which stays on the main
+/// type along with `persistWorkflows` and the `normalizedDomain(s)` helpers.
+extension DomainViewModel {
+ func workflow(withID id: UUID) -> DomainWorkflow? {
+ workflows.first(where: { $0.id == id })
+ }
+
+ func workflowsContaining(domain: String) -> [DomainWorkflow] {
+ let normalized = normalizedDomain(domain)
+ guard !normalized.isEmpty else { return [] }
+ return workflows.filter { workflow in
+ workflow.domains.contains(where: { $0.caseInsensitiveCompare(normalized) == .orderedSame })
+ }
+ }
+
+ func canEdit(_ workflow: DomainWorkflow) -> Bool {
+ workflow.collaboration?.canEdit ?? true
+ }
+
+ func canDelete(_ workflow: DomainWorkflow) -> Bool {
+ workflow.collaboration?.isOwner ?? true
+ }
+
+ func collaborationLabel(for workflow: DomainWorkflow) -> String? {
+ guard let collaboration = workflow.collaboration, collaboration.isShared else { return nil }
+ return "\(collaboration.ownership.title) • \(collaboration.permission.title)"
+ }
+
+ @discardableResult
+ func createWorkflow(name: String, domains: [String], notes: String? = nil) -> DomainWorkflow? {
+ let normalizedDomains = normalizedDomains(domains)
+ let trimmedName = name.trimmingCharacters(in: .whitespacesAndNewlines)
+ guard !trimmedName.isEmpty, !normalizedDomains.isEmpty else { return nil }
+ guard FeatureAccessService.canCreateWorkflow(currentCount: workflows.count) else {
+ upgradePrompt = FeatureAccessService.upgradePromptForWorkflows(currentCount: workflows.count)
+ return nil
+ }
+
+ let workflow = DomainWorkflow(
+ name: trimmedName,
+ domains: normalizedDomains,
+ createdAt: Date(),
+ updatedAt: Date(),
+ notes: notes?.trimmingCharacters(in: .whitespacesAndNewlines).nilIfEmpty,
+ collaboration: CollaborationMetadata(
+ scope: .privateDatabase,
+ ownership: .owner,
+ permission: .editable
+ )
+ )
+ workflows.insert(workflow, at: 0)
+ persistWorkflows()
+ return workflow
+ }
+
+ func updateWorkflow(_ workflow: DomainWorkflow, name: String, domains: [String], notes: String?) {
+ guard canEdit(workflow) else { return }
+ guard let index = workflows.firstIndex(where: { $0.id == workflow.id }) else { return }
+ let normalizedDomains = normalizedDomains(domains)
+ let trimmedName = name.trimmingCharacters(in: .whitespacesAndNewlines)
+ guard !trimmedName.isEmpty, !normalizedDomains.isEmpty else { return }
+
+ workflows[index].name = trimmedName
+ workflows[index].domains = normalizedDomains
+ workflows[index].notes = notes?.trimmingCharacters(in: .whitespacesAndNewlines).nilIfEmpty
+ workflows[index].updatedAt = Date()
+ persistWorkflows()
+ }
+
+ func deleteWorkflow(_ workflow: DomainWorkflow) {
+ guard canDelete(workflow) else { return }
+ CloudSyncService.shared.recordWorkflowDeletion(workflow)
+ workflows.removeAll { $0.id == workflow.id }
+ if latestWorkflowRunSummary?.workflowID == workflow.id {
+ latestWorkflowRunSummary = nil
+ }
+ if activeWorkflowRunID == workflow.id {
+ activeWorkflowRunID = nil
+ activeWorkflowRunName = nil
+ }
+ persistWorkflows()
+ }
+
+ func addDomains(_ domains: [String], to workflow: DomainWorkflow) {
+ guard canEdit(workflow) else { return }
+ guard let index = workflows.firstIndex(where: { $0.id == workflow.id }) else { return }
+ let mergedDomains = normalizedDomains(workflows[index].domains + domains)
+ guard mergedDomains != workflows[index].domains else { return }
+ workflows[index].domains = mergedDomains
+ workflows[index].updatedAt = Date()
+ persistWorkflows()
+ }
+
+ func removeWorkflowDomains(at offsets: IndexSet, from workflow: DomainWorkflow) {
+ guard canEdit(workflow) else { return }
+ guard let index = workflows.firstIndex(where: { $0.id == workflow.id }) else { return }
+ workflows[index].domains.remove(atOffsets: offsets)
+ workflows[index].updatedAt = Date()
+ persistWorkflows()
+ }
+
+ func moveWorkflowDomains(from offsets: IndexSet, to destination: Int, in workflow: DomainWorkflow) {
+ guard canEdit(workflow) else { return }
+ guard let index = workflows.firstIndex(where: { $0.id == workflow.id }) else { return }
+ workflows[index].domains.move(fromOffsets: offsets, toOffset: destination)
+ workflows[index].updatedAt = Date()
+ persistWorkflows()
+ }
+
+ func runWorkflow(_ workflow: DomainWorkflow) {
+ guard !workflow.domains.isEmpty else { return }
+ guard FeatureAccessService.canRunBatch(domainCount: workflow.domains.count) else {
+ upgradePrompt = FeatureAccessService.upgradePromptForBatch(domainCount: workflow.domains.count)
+ return
+ }
+ startBatchLookup(domains: workflow.domains, source: .workflow, workflow: workflow)
+ }
+
+ func rerunCurrentDomain(in workflow: DomainWorkflow) {
+ guard workflow.domains.contains(where: { $0.caseInsensitiveCompare(searchedDomain) == .orderedSame }) else {
+ return
+ }
+ runWorkflow(workflow)
+ }
+
+ func refreshWorkflowList() async {
+ workflows = Self.loadWorkflows()
+ await Task.yield()
+ }
+}
+
+private extension String {
+ var nilIfEmpty: String? {
+ isEmpty ? nil : self
+ }
+}
diff --git a/DomainDig/DomainViewModel.swift b/DomainDig/DomainViewModel.swift
index 250d46d..e0f649b 100644
--- a/DomainDig/DomainViewModel.swift
+++ b/DomainDig/DomainViewModel.swift
@@ -275,8 +275,8 @@ final class DomainViewModel {
private var lookupStartedAt: Date?
private var activeBatchDomains: [String] = []
private var lastBatchStartedAt: Date?
- private var activeWorkflowRunID: UUID?
- private var activeWorkflowRunName: String?
+ var activeWorkflowRunID: UUID?
+ var activeWorkflowRunName: String?
private var historyPersistenceSuspended = false
private var trackedDomainsPersistenceSuspended = false
private var historyPersistenceDirty = false
@@ -1195,18 +1195,6 @@ final class DomainViewModel {
}
}
- func workflow(withID id: UUID) -> DomainWorkflow? {
- workflows.first(where: { $0.id == id })
- }
-
- func workflowsContaining(domain: String) -> [DomainWorkflow] {
- let normalized = normalizedDomain(domain)
- guard !normalized.isEmpty else { return [] }
- return workflows.filter { workflow in
- workflow.domains.contains(where: { $0.caseInsensitiveCompare(normalized) == .orderedSame })
- }
- }
-
func canEdit(_ trackedDomain: TrackedDomain) -> Bool {
trackedDomain.collaboration?.canEdit ?? true
}
@@ -1220,121 +1208,6 @@ final class DomainViewModel {
return "\(collaboration.ownership.title) • \(collaboration.permission.title)"
}
- func canEdit(_ workflow: DomainWorkflow) -> Bool {
- workflow.collaboration?.canEdit ?? true
- }
-
- func canDelete(_ workflow: DomainWorkflow) -> Bool {
- workflow.collaboration?.isOwner ?? true
- }
-
- func collaborationLabel(for workflow: DomainWorkflow) -> String? {
- guard let collaboration = workflow.collaboration, collaboration.isShared else { return nil }
- return "\(collaboration.ownership.title) • \(collaboration.permission.title)"
- }
-
- @discardableResult
- func createWorkflow(name: String, domains: [String], notes: String? = nil) -> DomainWorkflow? {
- let normalizedDomains = normalizedDomains(domains)
- let trimmedName = name.trimmingCharacters(in: .whitespacesAndNewlines)
- guard !trimmedName.isEmpty, !normalizedDomains.isEmpty else { return nil }
- guard FeatureAccessService.canCreateWorkflow(currentCount: workflows.count) else {
- upgradePrompt = FeatureAccessService.upgradePromptForWorkflows(currentCount: workflows.count)
- return nil
- }
-
- let workflow = DomainWorkflow(
- name: trimmedName,
- domains: normalizedDomains,
- createdAt: Date(),
- updatedAt: Date(),
- notes: notes?.trimmingCharacters(in: .whitespacesAndNewlines).nilIfEmpty,
- collaboration: CollaborationMetadata(
- scope: .privateDatabase,
- ownership: .owner,
- permission: .editable
- )
- )
- workflows.insert(workflow, at: 0)
- persistWorkflows()
- return workflow
- }
-
- func updateWorkflow(_ workflow: DomainWorkflow, name: String, domains: [String], notes: String?) {
- guard canEdit(workflow) else { return }
- guard let index = workflows.firstIndex(where: { $0.id == workflow.id }) else { return }
- let normalizedDomains = normalizedDomains(domains)
- let trimmedName = name.trimmingCharacters(in: .whitespacesAndNewlines)
- guard !trimmedName.isEmpty, !normalizedDomains.isEmpty else { return }
-
- workflows[index].name = trimmedName
- workflows[index].domains = normalizedDomains
- workflows[index].notes = notes?.trimmingCharacters(in: .whitespacesAndNewlines).nilIfEmpty
- workflows[index].updatedAt = Date()
- persistWorkflows()
- }
-
- func deleteWorkflow(_ workflow: DomainWorkflow) {
- guard canDelete(workflow) else { return }
- CloudSyncService.shared.recordWorkflowDeletion(workflow)
- workflows.removeAll { $0.id == workflow.id }
- if latestWorkflowRunSummary?.workflowID == workflow.id {
- latestWorkflowRunSummary = nil
- }
- if activeWorkflowRunID == workflow.id {
- activeWorkflowRunID = nil
- activeWorkflowRunName = nil
- }
- persistWorkflows()
- }
-
- func addDomains(_ domains: [String], to workflow: DomainWorkflow) {
- guard canEdit(workflow) else { return }
- guard let index = workflows.firstIndex(where: { $0.id == workflow.id }) else { return }
- let mergedDomains = normalizedDomains(workflows[index].domains + domains)
- guard mergedDomains != workflows[index].domains else { return }
- workflows[index].domains = mergedDomains
- workflows[index].updatedAt = Date()
- persistWorkflows()
- }
-
- func removeWorkflowDomains(at offsets: IndexSet, from workflow: DomainWorkflow) {
- guard canEdit(workflow) else { return }
- guard let index = workflows.firstIndex(where: { $0.id == workflow.id }) else { return }
- workflows[index].domains.remove(atOffsets: offsets)
- workflows[index].updatedAt = Date()
- persistWorkflows()
- }
-
- func moveWorkflowDomains(from offsets: IndexSet, to destination: Int, in workflow: DomainWorkflow) {
- guard canEdit(workflow) else { return }
- guard let index = workflows.firstIndex(where: { $0.id == workflow.id }) else { return }
- workflows[index].domains.move(fromOffsets: offsets, toOffset: destination)
- workflows[index].updatedAt = Date()
- persistWorkflows()
- }
-
- func runWorkflow(_ workflow: DomainWorkflow) {
- guard !workflow.domains.isEmpty else { return }
- guard FeatureAccessService.canRunBatch(domainCount: workflow.domains.count) else {
- upgradePrompt = FeatureAccessService.upgradePromptForBatch(domainCount: workflow.domains.count)
- return
- }
- startBatchLookup(domains: workflow.domains, source: .workflow, workflow: workflow)
- }
-
- func rerunCurrentDomain(in workflow: DomainWorkflow) {
- guard workflow.domains.contains(where: { $0.caseInsensitiveCompare(searchedDomain) == .orderedSame }) else {
- return
- }
- runWorkflow(workflow)
- }
-
- func refreshWorkflowList() async {
- workflows = Self.loadWorkflows()
- await Task.yield()
- }
-
func runCustomPortScan(ports: [UInt16]) async {
guard !searchedDomain.isEmpty else {
customPortScanError = "Run a domain lookup first"
@@ -2361,7 +2234,7 @@ final class DomainViewModel {
trackedDomains.first { $0.domain.caseInsensitiveCompare(domain) == .orderedSame }
}
- private func normalizedDomain(_ domain: String) -> String {
+ func normalizedDomain(_ domain: String) -> String {
domain
.trimmingCharacters(in: .whitespacesAndNewlines)
.replacingOccurrences(of: "https://", with: "")
@@ -2438,7 +2311,7 @@ final class DomainViewModel {
return lookupID
}
- private func startBatchLookup(domains: [String], source: BatchLookupSource, workflow: DomainWorkflow? = nil) {
+ func startBatchLookup(domains: [String], source: BatchLookupSource, workflow: DomainWorkflow? = nil) {
guard !domains.isEmpty else { return }
guard !batchLookupRunning else { return }
@@ -3516,13 +3389,13 @@ final class DomainViewModel {
return DomainDataPortabilityService.loadTrackedDomains()
}
- private func persistWorkflows() {
+ func persistWorkflows() {
DomainDataPortabilityService.saveWorkflows(workflows)
CloudSyncService.shared.scheduleSyncIfNeeded()
refreshDataLifecycleSummary()
}
- private static func loadWorkflows() -> [DomainWorkflow] {
+ static func loadWorkflows() -> [DomainWorkflow] {
DataMigrationService.migrateIfNeeded()
return DomainDataPortabilityService.loadWorkflows()
}
@@ -3535,7 +3408,7 @@ final class DomainViewModel {
}
}
- private func normalizedDomains(_ domains: [String]) -> [String] {
+ func normalizedDomains(_ domains: [String]) -> [String] {
var seen = Set<String>()
return domains
.map(normalizedDomain)