Skip to content

refactor: migrate server actions to ActionResult<T> (#102)#112

Merged
ryota-murakami merged 3 commits intomainfrom
refactor/issue-102-action-result-migration
Feb 13, 2026
Merged

refactor: migrate server actions to ActionResult<T> (#102)#112
ryota-murakami merged 3 commits intomainfrom
refactor/issue-102-action-result-migration

Conversation

@ryota-murakami
Copy link
Contributor

@ryota-murakami ryota-murakami commented Feb 13, 2026

Summary

  • Migrate client-consumed server actions from throw-based error handling to ActionResult<T> discriminated union pattern
  • board.ts: Convert createBoard, updateBoardPositions to use withAuthResult<T>
  • project-info.ts / maintenance-project-info.ts: All 12 thin wrappers now return ActionResult<T> with try/catch around Core calls
  • repo-cards.ts: deleteRepoCard typed as ActionResult<void>
  • Update all callers (CreateBoardForm, BoardGrid, KanbanBoard, useNoteModal, board-data, MaintenanceClient) to check result.success instead of try/catch

Test plan

  • pnpm typecheck passes
  • pnpm lint passes
  • pnpm test passes (1203 tests, including updated useNoteModal.test.ts)
  • pnpm e2e in CI (12 shards)

Closes #102

Summary by CodeRabbit

  • Bug Fixes

    • More reliable board creation flow with clear success and error toasts.
    • Improved detection and feedback when board position or comment operations fail; UI now reverts changes and shows error messages when persistence fails.
    • Maintenance and note modals now avoid showing stale data on fetch failures.
  • Refactor

    • Unified response handling across board, comment, and project-info flows for more consistent user-facing behavior.

…on (#102)

Convert client-consumed server actions from throw-based to ActionResult<T> pattern:
- board.ts: createBoard, updateBoardPositions
- project-info.ts: all 6 wrappers (getProjectInfo, upsertProjectInfo, etc.)
- maintenance-project-info.ts: all 6 wrappers
- repo-cards.ts: deleteRepoCard type annotation

Update all callers to check result.success instead of try/catch.
@vercel
Copy link
Contributor

vercel bot commented Feb 13, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
gitbox Ready Ready Preview, Comment Feb 13, 2026 10:26am

Request Review

@coderabbitai
Copy link

coderabbitai bot commented Feb 13, 2026

📝 Walkthrough

Walkthrough

Migrates many server actions to an ActionResult discriminated union, updates callers to check result.success and use result.data/error, removes some direct Sentry captures (and adds centralized Sentry in new wrappers), and adjusts tests and UI flows to the new response envelope and navigation/toast behavior.

Changes

Cohort / File(s) Summary
Server Actions — Core
src/lib/actions/board.ts, src/lib/actions/project-info.ts, src/lib/actions/maintenance-project-info.ts, src/lib/actions/repo-cards.ts
Convert server actions to return ActionResult<T>; wrap logic with withAuthResult/try-catch where applicable; add/centralize Sentry reporting in some modules; update function signatures and return shapes.
Consumer UI — Boards & Kanban
src/app/boards/new/CreateBoardForm.tsx, src/components/Board/KanbanBoard.tsx, src/components/Boards/BoardGrid.tsx
Replace try/catch with result.success checks when calling actions; use result.data on success and result.error on failure; update navigation/toasts and optimistic update rollback points.
Hooks & Maintenance UIs
src/hooks/board/useNoteModal.ts, src/app/maintenance/hooks/useMaintenanceComments.ts, src/app/maintenance/hooks/useMaintenanceNoteModal.ts
Unwrap new result envelope from project-info/maintenance actions; guard state updates on result.success and default to empty state on failures.
Action Helpers / Data Fetch
src/lib/actions/board-data.ts, src/lib/actions/shared-project-info.ts
Refactor comment-batch handling to check commentsResult.success and derive data; remove Sentry captures from shared core functions and switch to throwing on failures.
Tests
src/tests/unit/hooks/useNoteModal.test.ts
Update mocks to { success, data } envelope, remove Sentry mocks, and adjust assertions and failure simulations to the ActionResult pattern.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant UI as Component (Create/Board/Kanban)
  participant Action as Server Action (withAuthResult)
  participant DB
  participant Sentry

  rect rgba(92,123,255,0.5)
  User->>UI: click create/update/comment action
  UI->>Action: call action(...) (await)
  end

  rect rgba(16,185,129,0.5)
  Action->>DB: perform DB ops / validation
  DB-->>Action: success / failure
  end

  alt DB success
    Action-->>UI: { success: true, data: ... }
    UI->>UI: apply success flow (navigate/toast/update state)
  else DB failure
    Action->>Sentry: capture exception / context
    Action-->>UI: { success: false, error: "..." }
    UI->>UI: revert optimistic update / show error toast
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

A Result returned, no thrown surprise,
success or error—clear in plain eyes.
UI listens closely, toasts sing the tune,
DB and Sentry dance beneath the moon.
Type-safe unions: neat, concise, and bright ✨

🚥 Pre-merge checks | ✅ 6
✅ Passed checks (6 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'refactor: migrate server actions to ActionResult (#102)' clearly and concisely describes the main change: migrating server actions to use the ActionResult pattern.
Linked Issues check ✅ Passed The PR successfully implements all objectives from #102: migrated board.ts (createBoard, updateBoardPositions), project-info.ts and maintenance-project-info.ts (all wrapper functions), repo-cards.ts (deleteRepoCard), and updated all callers to use result.success pattern.
Out of Scope Changes check ✅ Passed All changes align with #102 scope: converted client-facing server actions to ActionResult, updated callers accordingly, and preserved throw-based behavior for internal/server-component-only functions and auth.ts as required.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/issue-102-action-result-migration

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.

@codecov-commenter
Copy link

codecov-commenter commented Feb 13, 2026

Codecov Report

❌ Patch coverage is 30.76923% with 18 lines in your changes missing coverage. Please review.
✅ Project coverage is 73.43%. Comparing base (2740b01) to head (552a845).

Files with missing lines Patch % Lines
...c/app/maintenance/hooks/useMaintenanceNoteModal.ts 0.00% 7 Missing ⚠️
src/app/boards/new/CreateBoardForm.tsx 0.00% 5 Missing ⚠️
src/components/Board/KanbanBoard.tsx 0.00% 4 Missing ⚠️
src/components/Boards/BoardGrid.tsx 33.33% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #112      +/-   ##
==========================================
+ Coverage   73.29%   73.43%   +0.13%     
==========================================
  Files         137      137              
  Lines        4116     4115       -1     
  Branches     1065     1073       +8     
==========================================
+ Hits         3017     3022       +5     
+ Misses       1078     1072       -6     
  Partials       21       21              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link

@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

🤖 Fix all issues with AI agents
In `@src/lib/actions/maintenance-project-info.ts`:
- Around line 54-72: The wrapper functions like getMaintenanceProjectInfo are
duplicating Sentry reporting because getProjectInfoCore (in
shared-project-info.ts) already calls Sentry.captureException; to centralize
reporting at the ActionResult/wrapper layer (preferred), remove
Sentry.captureException calls from the core helpers (e.g., getProjectInfoCore
and the other core functions referenced by the six wrapper functions in this
file) so the core just throws errors, and keep the existing
Sentry.captureException and ActionResult handling in the wrappers
(getMaintenanceProjectInfo and the other five functions) to ensure a single
capture point.
🧹 Nitpick comments (6)
src/lib/actions/board.ts (1)

568-590: updateBoardPositions — looks good.

One minor observation: Line 583 maps the original updates array (pre-validation) into the RPC call rather than validation.data. Since safeParse can strip/transform values, using validation.data would be more defensive.

Proposed fix
-  return withAuthResult(async (supabase) => {
-    // Atomic batch update via PostgreSQL RPC — all updates succeed or all fail
-    // Note: RLS policies on board table still enforce user ownership
-    const { error } = await supabase.rpc('batch_update_board_positions', {
-      p_updates: updates.map(({ id, position }) => ({ id, position })),
-    })
+  return withAuthResult(async (supabase) => {
+    const validatedUpdates = validation.data
+    // Atomic batch update via PostgreSQL RPC — all updates succeed or all fail
+    // Note: RLS policies on board table still enforce user ownership
+    const { error } = await supabase.rpc('batch_update_board_positions', {
+      p_updates: validatedUpdates.map(({ id, position }) => ({ id, position })),
+    })
src/app/boards/new/CreateBoardForm.tsx (1)

40-51: Variable result shadows the outer result from line 31.

Both the Zod safeParse result (line 31) and the createBoard return (line 40) are named result. This compiles fine since the outer one isn't needed inside startTransition, but it's a readability trip hazard. Consider renaming one (e.g., createResult or boardResult).

src/hooks/board/useNoteModal.ts (1)

119-130: save ignores the ActionResult from upsertProjectInfo.

upsertProjectInfo now returns ActionResult<void>, but the result is discarded. If the save fails, the user gets no feedback. Consider checking result.success and surfacing an error (e.g., via toast).

Proposed fix
   const save = useCallback(
     async (note: string, links: ProjectLink[]) => {
       if (!cardId) return

-      await upsertProjectInfo(cardId, {
+      const result = await upsertProjectInfo(cardId, {
         note,
         comment: '',
         links,
       })
+      if (!result.success) {
+        // Surface error to user — consider toast or callback
+        console.error('Failed to save project info:', result.error)
+      }
     },
     [cardId],
   )
src/components/Board/KanbanBoard.tsx (2)

299-302: Empty failure branch — optimistic update is never rolled back and user gets no feedback.

The old code had try/catch with Sentry.captureException. Now the server-side Sentry capture still happens (inside the wrapper), but the client silently keeps stale optimistic state on failure. The // Could implement rollback here if needed comment acknowledges the gap.

For consistency with BoardGrid.handleDragEnd (which reverts state + shows a toast on failure), consider at least notifying the user:

Proposed fix
       const result = await updateComment(cardId, newComment)
       if (!result.success) {
-        // Could implement rollback here if needed
+        // Rollback optimistic update
+        setComments((prev) => ({
+          ...prev,
+          [cardId]: {
+            comment: prev[cardId]?.comment ?? '',
+            color: prev[cardId]?.color ?? 'primary',
+          },
+        }))
+        toast.error('Failed to update comment')
       }

325-325: handleCommentColorChange and handleCommentDelete silently discard the ActionResult.

Both apply optimistic updates but never check the result, so failures are invisible to the user. This is inconsistent with handleCommentChange (which at least captures result) and with BoardGrid (which reverts + toasts).

Proposed fix for handleCommentColorChange
       // Persist to database
-      await updateCommentColor(cardId, color)
+      const result = await updateCommentColor(cardId, color)
+      if (!result.success) {
+        toast.error('Failed to update comment color')
+      }
Proposed fix for handleCommentDelete
       // Persist to database
-      await deleteComment(cardId)
+      const result = await deleteComment(cardId)
+      if (!result.success) {
+        toast.error('Failed to delete comment')
+      }

Also applies to: 347-347

src/lib/actions/maintenance-project-info.ts (1)

54-237: Consider a generic helper to eliminate the repeated try/catch boilerplate.

All 6 functions follow an identical pattern: try { await coreFunction(...); return { success, data } } catch { Sentry; return { success: false, error } }. A small utility could DRY this up:

async function wrapAction<T>(
  fn: () => Promise<T>,
  context: string,
  extra?: Record<string, unknown>,
): Promise<ActionResult<T>> {
  try {
    const data = await fn()
    return { success: true, data }
  } catch (error) {
    // Only if you keep Sentry here (see other comment)
    Sentry.captureException(error, { extra: { context, ...extra } })
    return {
      success: false,
      error: error instanceof Error ? error.message : `Failed: ${context}`,
    }
  }
}

Then each wrapper becomes a one-liner. Not blocking — the current code is clear and correct.

- Remove Sentry.captureException from shared-project-info.ts core functions
- Centralize error reporting at ActionResult wrapper layer only
- Fix callers to unwrap ActionResult before using data
- Make getCommentsCore throw on error (consistent with other core functions)
@morph-subagents
Copy link

🤖 Morph Preview Test

Looks like you hit your rate limits!

Please upgrade your limits here, or wait a few minutes and try again.

If you need help, reach out to support@morphllm.com.


Automated testing by Morph

@github-actions
Copy link

github-actions bot commented Feb 13, 2026

🧪 E2E Coverage Report (Sharded: 12 parallel jobs)

Metric Coverage
Lines 94.38%
Functions 17.73%
Branches 16.92%
Statements 30.51%

📊 Full report available in workflow artifacts

Apply ActionResult<T> unwrapping to extracted hooks instead of
inline code. getCommentsForMaintenanceItems and getMaintenanceProjectInfo
now return ActionResult — callers check .success before accessing .data.
@morph-subagents
Copy link

🤖 Morph Preview Test

Looks like you hit your rate limits!

Please upgrade your limits here, or wait a few minutes and try again.

If you need help, reach out to support@morphllm.com.


Automated testing by Morph

Copy link

@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 (2)
src/app/maintenance/hooks/useMaintenanceNoteModal.ts (1)

82-97: ⚠️ Potential issue | 🟠 Major

Check upsertMaintenanceProjectInfo result before closing the modal — save failures are silently swallowed.

upsertMaintenanceProjectInfo returns ActionResult<void>, but line 86 discards the result. The modal closes and state resets unconditionally (lines 91–94), leaving the user with no feedback if the save failed. This is inconsistent with how getMaintenanceProjectInfo is handled in openNoteModal (which checks result.success).

Suggested fix
     async (note: string, links: ProjectLink[]) => {
       if (!selectedRepoForNote) return
       const currentComment = comments[selectedRepoForNote.id]?.comment || ''
-      await upsertMaintenanceProjectInfo(selectedRepoForNote.id, {
+      const result = await upsertMaintenanceProjectInfo(selectedRepoForNote.id, {
         note,
         comment: currentComment,
         links,
       })
+      if (!result.success) {
+        // surface the error (toast, console, etc.) instead of silently closing
+        console.error(result.error)
+        return
+      }
       setNoteModalOpen(false)
       setSelectedRepoForNote(null)
       setCurrentNote('')
       setCurrentLinks([])
     },
src/lib/actions/shared-project-info.ts (1)

38-50: ⚠️ Potential issue | 🟡 Minor

Remove stale JSDoc reference to Sentry for the label field.

The label parameter is not used for Sentry error context (which is not present in this module). Currently, the label field is configuration-only and unused throughout the core functions. Update the JSDoc to clarify its actual purpose, or remove it if it's truly unused:

Suggested update
/**
 * Foreign key configuration for project info operations
 *
 * `@param` column - The FK column name in the projectinfo table
- * `@param` label - Human-readable label for Sentry error context
+ * `@param` label - Human-readable label for context and logging

If label is not intended to be used, consider removing the field entirely to avoid future confusion.

🤖 Fix all issues with AI agents
In `@src/app/maintenance/hooks/useMaintenanceComments.ts`:
- Around line 47-49: The fetch in useMaintenanceComments silently ignores
failures; update the getCommentsForMaintenanceItems.then callback to handle the
!result.success case by logging the error and notifying the user: when
result.success is false call console.error(result.error) (or a logger) and
trigger a user-visible notification (e.g., a toast or set an error state)
instead of doing nothing; keep the current setComments(result.data) path
unchanged for success so the logic around setComments remains intact.
🧹 Nitpick comments (1)
src/app/maintenance/hooks/useMaintenanceComments.ts (1)

63-81: Optimistic updates are never rolled back on server failure.

updateMaintenanceComment, updateMaintenanceCommentColor, and deleteMaintenanceComment now return ActionResult, but the result is discarded in all three handlers (lines 75, 102, 115). If the server call fails, the UI keeps the optimistic state with no rollback or user notification.

Consider checking the result and reverting state on failure, e.g.:

Sketch for handleCommentSave
     async (repoId: string, newComment: string, options: CommentSaveOptions) => {
+      const prev = comments
       setComments((prev) => ({
         ...prev,
         [repoId]: {
           comment: newComment,
           color: prev[repoId]?.color ?? 'neutral',
         },
       }))
-      await updateMaintenanceComment(repoId, newComment)
+      const result = await updateMaintenanceComment(repoId, newComment)
+      if (!result.success) {
+        setComments(prev)
+        // optionally toast result.error
+      }
       if (options.closeOnSave) {
         setEditingCommentId(null)
       }
     },

Same pattern applies to handleColorChange (line 102) and handleCommentDelete (line 115).

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.

Migrate remaining Server Actions to ActionResult<T> pattern

2 participants