Skip to content

Extract TranscriptionHistoryViewModel; remove dead UsageDashboardView (A2)#21

Merged
jtn0123 merged 2 commits into
masterfrom
a2-history-viewmodel
May 17, 2026
Merged

Extract TranscriptionHistoryViewModel; remove dead UsageDashboardView (A2)#21
jtn0123 merged 2 commits into
masterfrom
a2-history-viewmodel

Conversation

@jtn0123
Copy link
Copy Markdown
Owner

@jtn0123 jtn0123 commented May 17, 2026

Summary

Grade-report item A2 — views bypassing the ViewModel layer to call store singletons directly. Done as its own focused PR.

  • Removed the orphaned UsageDashboardView. After the dashboard redesign, DashboardView's Overview tab switched to DashboardHomeView; UsageDashboardView was left referenced by nothing but its own test. Deleting the dead view + test also eliminates four direct store-singleton accesses.
  • Extracted TranscriptionHistoryViewModel. TranscriptionHistoryView called DataManager.shared directly for fetch / pagination / delete. That logic and state now live in a @MainActor @Observable ViewModel, constructor-injected with a DataManagerProtocol (default DataManager.shared) — mirroring the existing RecordingViewModel / DashboardHomeView injection pattern. The view keeps only UI state and no longer touches the store. Adds 10 ViewModel tests using MockDataManager.

The remaining .shared references the audit noted (in DashboardView / DashboardHomeView / DashboardCategoriesView) are already injectable-default seams (x ?? .shared), not true layering violations — left as-is.

Test plan

  • swift build + swift build --build-tests clean
  • swiftlint --strict clean
  • Full suite: 2749 tests, 0 failures, 37 skipped (count down from 2779 — the dead UsageDashboardView tests were removed)

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • Refactor

    • Refactored transcription history with improved state management and pagination handling.
  • Removals

    • Removed usage dashboard view.

Review Change Stack

jtn0123 and others added 2 commits May 17, 2026 14:39
After the dashboard redesign, DashboardView's Overview tab switched to
DashboardHomeView and UsageDashboardView was left referenced by nothing
but its own test. Deleting the dead view (and its test) also removes
four direct store-singleton accesses. (grade-report A2)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
TranscriptionHistoryView called DataManager.shared directly for record
fetching, pagination, and deletion. That logic and state now live in a
@mainactor @observable TranscriptionHistoryViewModel, constructor-
injected with a DataManagerProtocol (default DataManager.shared) so it
is unit-testable with MockDataManager. The view keeps only UI state and
no longer touches the store. Adds 10 ViewModel tests. (grade-report A2)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 17, 2026

📝 Walkthrough

Walkthrough

This PR extracts pagination and record-management logic from TranscriptionHistoryView into a new @Observable TranscriptionHistoryViewModel. The viewmodel owns pagination state, loading flags, and error reporting, while the view delegates all data operations and renders from the viewmodel's published state. Dashboard view and tests are also removed in this change.

Changes

Transcription History View Model Extraction

Layer / File(s) Summary
ViewModel Implementation
Sources/ViewModels/TranscriptionHistoryViewModel.swift
TranscriptionHistoryViewModel is introduced as a @MainActor @Observable class that owns pagination state (records, page, hasMore, isLoading, hasLoadedOnce), error state (showError, errorMessage), and implements loadRecords(reset:search:) for paginated fetching with search normalization, deleteRecord(_:search:) for single-record deletion followed by reload, and clearAllRecords(search:) for bulk deletion with explicit loading-state management.
View Refactoring to Use ViewModel
Sources/Views/Transcription/TranscriptionHistoryView.swift
TranscriptionHistoryView is refactored to hold a @State viewmodel instance and remove internal pagination/deletion logic. Loading/empty-state branching, list rendering, pagination-trigger callbacks, delete confirmation, and error alert now depend on viewModel.isLoading, viewModel.records, viewModel.showError, and related properties. Subtitle count and the trailing "+" indicator are computed from viewModel.records.count and viewModel.hasMore.
ViewModel Test Suite
Tests/ViewModels/TranscriptionHistoryViewModelTests.swift
Comprehensive test coverage for pagination (reset, page advancement, hasMore transitions), loading behavior (empty results still mark as loaded, search text trimming), record deletion and bulk clearing (state updates and reload), and error paths (fetch/delete/clear-all exceptions set showError and errorMessage consistently).
Dashboard Removal
Sources/Views/Dashboard/UsageDashboardView.swift, Tests/Views/Dashboard/UsageDashboardViewTests.swift
UsageDashboardView, UsageMetricCard, and the entire test suite covering usage metrics display, reset confirmation, and formatting utilities are removed.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A viewmodel hops into view,
Taking pagination's load with stride,
The view dances lighter now, renewed,
While tests verify each bound and slide.
And dashboards fade to morning dew.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 44.44% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the two main changes: extracting TranscriptionHistoryViewModel and removing the dead UsageDashboardView, which directly match the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch a2-history-viewmodel

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Sources/Views/Transcription/TranscriptionHistoryView.swift (1)

22-97: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Split body into smaller view-builder units to satisfy function-length rule.

body is still substantially over the 40-line limit, which makes this file harder to maintain despite the VM extraction.

As per coding guidelines, Keep functions to 40 lines or fewer.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Views/Transcription/TranscriptionHistoryView.swift` around lines 22 -
97, The body of TranscriptionHistoryView is overlong; split it into smaller
view-builder units by extracting logical sections into private computed
properties or funcs (e.g., makeHeader(), makeSearchBar(), makeContentView(),
makeAlertsAndModifiers()) and call those from body to keep each function under
40 lines. Move the TranscriptionHistoryHeader, TranscriptionSearchBar, the
conditional records/list block, and the alert/modifier chains into their
respective helpers (preserve bindings like $searchText, $isSearchFocused,
expandedRecords and callbacks such as toggleExpansion(for:), confirmDelete,
copyToClipboard, handleEscapeKey, handleCommandF) and ensure .task and .onChange
remain applied to the main container or appropriate helper so behavior is
unchanged. Ensure helpers return some View and are marked private to keep class
scope clean.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/ViewModels/TranscriptionHistoryViewModel.swift`:
- Around line 42-43: The guard that returns when isLoading is true discards
incoming reset requests; instead add a queued-reset mechanism on
TranscriptionHistoryViewModel: introduce a property like pendingReset (and
optionally pendingQuery or pendingParameters) and, in the method that currently
contains "guard !isLoading else { return }", if isLoading is true set
pendingReset = true (and store the latest query params) and return without
dropping the request; after the current fetch completes (where you set isLoading
= false) check pendingReset and if set clear it and replay the load call with
reset: true (using the stored params if applicable) so the latest reset is
executed when the in-flight load finishes.

---

Outside diff comments:
In `@Sources/Views/Transcription/TranscriptionHistoryView.swift`:
- Around line 22-97: The body of TranscriptionHistoryView is overlong; split it
into smaller view-builder units by extracting logical sections into private
computed properties or funcs (e.g., makeHeader(), makeSearchBar(),
makeContentView(), makeAlertsAndModifiers()) and call those from body to keep
each function under 40 lines. Move the TranscriptionHistoryHeader,
TranscriptionSearchBar, the conditional records/list block, and the
alert/modifier chains into their respective helpers (preserve bindings like
$searchText, $isSearchFocused, expandedRecords and callbacks such as
toggleExpansion(for:), confirmDelete, copyToClipboard, handleEscapeKey,
handleCommandF) and ensure .task and .onChange remain applied to the main
container or appropriate helper so behavior is unchanged. Ensure helpers return
some View and are marked private to keep class scope clean.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6a2e447d-0a74-4ca9-a67b-82ec768b78d4

📥 Commits

Reviewing files that changed from the base of the PR and between d24a40b and 0aaa9bc.

📒 Files selected for processing (5)
  • Sources/ViewModels/TranscriptionHistoryViewModel.swift
  • Sources/Views/Dashboard/UsageDashboardView.swift
  • Sources/Views/Transcription/TranscriptionHistoryView.swift
  • Tests/ViewModels/TranscriptionHistoryViewModelTests.swift
  • Tests/Views/Dashboard/UsageDashboardViewTests.swift
💤 Files with no reviewable changes (2)
  • Tests/Views/Dashboard/UsageDashboardViewTests.swift
  • Sources/Views/Dashboard/UsageDashboardView.swift

Comment on lines +42 to +43
guard !isLoading else { return }
isLoading = true
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reset search requests can be dropped while loading, causing stale results.

At Line 42, guard !isLoading else { return } discards any reset: true call fired during an in-flight fetch. With rapid search updates, the latest query may never load.

💡 Suggested fix (queue latest reset and replay after current load)
 final class TranscriptionHistoryViewModel {
+    private var pendingResetSearch: String?
@@
     func loadRecords(reset: Bool = false, search: String = "") async {
-        guard !isLoading else { return }
+        if isLoading {
+            if reset { pendingResetSearch = search }
+            return
+        }
         isLoading = true
         defer {
             isLoading = false
             hasLoadedOnce = true
         }
@@
         do {
@@
         } catch {
             errorMessage = "Failed to load transcription history: \(error.localizedDescription)"
             showError = true
             hasMore = false
         }
+
+        if let queuedSearch = pendingResetSearch {
+            pendingResetSearch = nil
+            Task { [weak self] in
+                await self?.loadRecords(reset: true, search: queuedSearch)
+            }
+        }
     }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/ViewModels/TranscriptionHistoryViewModel.swift` around lines 42 - 43,
The guard that returns when isLoading is true discards incoming reset requests;
instead add a queued-reset mechanism on TranscriptionHistoryViewModel: introduce
a property like pendingReset (and optionally pendingQuery or pendingParameters)
and, in the method that currently contains "guard !isLoading else { return }",
if isLoading is true set pendingReset = true (and store the latest query params)
and return without dropping the request; after the current fetch completes
(where you set isLoading = false) check pendingReset and if set clear it and
replay the load call with reset: true (using the stored params if applicable) so
the latest reset is executed when the in-flight load finishes.

@sonarqubecloud
Copy link
Copy Markdown

@jtn0123 jtn0123 merged commit b8f3cf1 into master May 17, 2026
9 of 10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant