Skip to content

Conversation

@Brendonovich
Copy link
Contributor

@Brendonovich Brendonovich commented Aug 12, 2025

The commit replaces local loading states and manual error handling with React Query mutations across multiple components for better state management and consistency.

Summary by CodeRabbit

  • Improvements

    • Unified mutation-driven flows for notifications, caps, sharing, custom domains, and email sign-in with consistent loading/disabled states, spinners, and toasts.
    • Cap actions (download, delete, duplicate) now show progress labels and toast-based download UX.
  • Behavior Changes

    • Duplication no longer forces an automatic page refresh.
    • Folder delete confirmation no longer auto-closes after success.
  • Refactor

    • Replaced ad-hoc async patterns with centralized mutation patterns for consistent UI feedback.
  • Chores

    • Removed legacy ESLint root config.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 12, 2025

Walkthrough

Replace local try/catch and Effect.fn flows with React Query mutations across notifications, caps, sharing, custom domain, and login; comment out lifecycle hook wiring in effect-react-query. UI loading/disabled states now derive from mutation.isPending; many onSuccess/onError callbacks converted to plain functions.

Changes

Cohort / File(s) Summary of changes
Notifications
apps/web/app/(org)/dashboard/_components/Notifications/NotificationHeader.tsx
Replace local loading state with a useMutation for mark-as-read; use mutation callbacks and invalidateQueries("notifications"); UI disabled state bound to mutation.isPending.
Caps management (cards, lists, folder flows)
apps/web/app/(org)/dashboard/caps/Caps.tsx, apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx, apps/web/app/(org)/dashboard/caps/components/Folder.tsx, apps/web/app/(org)/dashboard/caps/components/SelectedCapsBar.tsx, apps/web/app/(org)/dashboard/folder/[id]/components/FolderVideosSection.tsx
Convert ad-hoc Promise/Effect.fn flows to useMutation/useEffectMutation patterns for download, delete, duplicate, and bulk-delete; bind dialogs/buttons to mutation isPending; simplify click handlers; remove some automatic dialog-closing behavior (Folder).
Sharing dialog
apps/web/app/(org)/dashboard/caps/components/SharingDialog.tsx
Use useMutation for sharing updates; derive loading from isPending; compute added/removed/public diffs to show granular toasts; restore init logic via useEffect; Save invokes mutation.mutate.
Custom domain (settings & dialog)
apps/web/app/(org)/dashboard/settings/organization/components/CustomDomain.tsx, apps/web/app/(org)/dashboard/settings/organization/components/CustomDomainDialog/CustomDomainDialog.tsx
Introduce useMutation for remove/update/check domain flows; mutationFns encapsulate sequential logic (remove then update); onSuccess/onError converted to plain callbacks; UI spinners/disabled states driven by isPending; router refreshes and toasts moved into mutation callbacks.
Login (email)
apps/web/app/(org)/login/form.tsx
Add emailSignInMutation to handle email sign-in flow and analytics; derive emailSent and loading from mutation state; use mutation.reset() for restart.
Effect React Query integration
apps/web/lib/effect-react-query.ts
Comment out public option declarations and runtime wiring for onMutate, onSuccess, onError, and onSettled in useEffectMutation; mutationFn path remains but lifecycle hook invocation is disabled.
Tooling config
.eslintrc.ts
Deleted root ESLint config file (module.exports removed).

Sequence Diagram(s)

sequenceDiagram
  actor User
  participant UI as Component
  participant RQ as React Query
  participant API as Server/API
  participant UX as Router/Toast

  User->>UI: trigger action (download/delete/mark/share/etc.)
  UI->>RQ: mutate(variables)
  RQ->>API: call mutationFn(variables)
  API-->>RQ: result / error
  RQ-->>UI: update isPending / isSuccess / isError
  UI->>UX: invalidate/refresh queries, show toast, update UI state
Loading
sequenceDiagram
  actor User
  participant Dialog as CustomDomainDialog
  participant Update as updateDomainMutation
  participant Check as checkDomainMutation
  participant API as Domain API
  participant UX as Router/Toast

  User->>Dialog: Submit domain
  Dialog->>Update: mutate({ domain, orgId })
  Update->>API: remove existing domain? + update
  API-->>Update: response
  Update-->>Dialog: success -> handleNext + toast + refresh

  User->>Dialog: Check verification
  Dialog->>Check: mutate({ orgId, showToasts })
  Check->>API: verify domain
  API-->>Check: { verified, config }
  Check-->>Dialog: setIsVerified + setDomainConfig + optional toast
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

I’m a rabbit tapping keys in a breeze,
Mutations hop in, tidy the trees.
Toasts chirp, spinners twirl in time,
Queries refresh — all neat and primed.
Hop, hop, shipped — the dashboard’s sublime. 🐇✨


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 21bbfe9 and e4329e5.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (1)
  • apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx (8 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build Desktop (aarch64-apple-darwin, macos-latest)
  • GitHub Check: Build Desktop (x86_64-pc-windows-msvc, windows-latest)
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch more-useMutation

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@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: 8

🔭 Outside diff range comments (2)
apps/web/app/(org)/dashboard/caps/components/Folder.tsx (1)

258-267: Align spaceId fallback strategy across drop handlers

The mobile drop target handler uses a fallback for spaceId (spaceId ?? activeOrganization?.organization.id), but the desktop drop handler does not. This inconsistency can cause moveVideoToFolder to receive undefined spaceId in one path and a valid fallback in the other.

Consider aligning the desktop drop to use the same fallback:

-      await moveVideoToFolder({ videoId: capData.id, folderId: id, spaceId });
+      await moveVideoToFolder({
+        videoId: capData.id,
+        folderId: id,
+        spaceId: spaceId ?? activeOrganization?.organization.id,
+      });
apps/web/lib/effect-react-query.ts (1)

268-268: Remove debug console.log statement

Debug logging should be removed from production code.

-              console.log({ result });
♻️ Duplicate comments (1)
apps/web/app/(org)/dashboard/folder/[id]/components/FolderVideosSection.tsx (1)

90-93: Inconsistent callback type with effect-react-query expectations

Similar to the issue in Caps.tsx, the onSuccess callback is now a plain function but the type system expects an Effect-wrapped function.

This needs to be wrapped in Effect or the type definitions need to be updated to accept plain functions.

🧹 Nitpick comments (3)
apps/web/app/(org)/dashboard/caps/components/Folder.tsx (1)

252-256: Comment contradicts behavior ("Keep the folder open" vs playing "folder-close")

The comment says to keep the folder open, but the code plays the "folder-close" animation. Update the comment or animation to match intent.

apps/web/app/(org)/login/form.tsx (1)

58-63: Avoid relying on outer email state inside onSuccess; use mutation variables

Using the variables parameter ensures the tracked domain matches the email submitted for this mutation, even if state changes between submit and resolution.

-    onSuccess: () => {
-      trackEvent("auth_email_sent", {
-        email_domain: email.split("@")[1],
-      });
+    onSuccess: (_result, submittedEmail) => {
+      const domain = submittedEmail?.includes("@")
+        ? submittedEmail.split("@")[1]
+        : undefined;
+      trackEvent("auth_email_sent", {
+        ...(domain ? { email_domain: domain } : {}),
+      });
       toast.success("Email sent - check your inbox!");
     },
apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx (1)

124-134: Close dialog on settle is good; consider adding a success toast

The onSettled handler reliably closes the dialog. Consider adding an onSuccess toast to confirm the deletion to users (optional).

   const deleteMutation = useMutation({
     mutationFn: async () => {
       await onDelete?.();
     },
     onError: (error) => {
       console.error("Error deleting cap:", error);
     },
+    onSuccess: () => {
+      toast.success("Cap deleted");
+    },
     onSettled: () => {
       setConfirmOpen(false);
     },
   });
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 038e153 and 61594ae.

📒 Files selected for processing (11)
  • apps/web/app/(org)/dashboard/_components/Notifications/NotificationHeader.tsx (1 hunks)
  • apps/web/app/(org)/dashboard/caps/Caps.tsx (1 hunks)
  • apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx (7 hunks)
  • apps/web/app/(org)/dashboard/caps/components/Folder.tsx (3 hunks)
  • apps/web/app/(org)/dashboard/caps/components/SelectedCapsBar.tsx (1 hunks)
  • apps/web/app/(org)/dashboard/caps/components/SharingDialog.tsx (8 hunks)
  • apps/web/app/(org)/dashboard/folder/[id]/components/FolderVideosSection.tsx (1 hunks)
  • apps/web/app/(org)/dashboard/settings/organization/components/CustomDomain.tsx (4 hunks)
  • apps/web/app/(org)/dashboard/settings/organization/components/CustomDomainDialog/CustomDomainDialog.tsx (7 hunks)
  • apps/web/app/(org)/login/form.tsx (5 hunks)
  • apps/web/lib/effect-react-query.ts (2 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (7)
apps/web/app/(org)/dashboard/settings/organization/components/CustomDomain.tsx (1)
apps/web/actions/organization/remove-domain.ts (1)
  • removeOrganizationDomain (9-59)
apps/web/app/(org)/dashboard/_components/Notifications/NotificationHeader.tsx (1)
apps/web/actions/notifications/mark-as-read.ts (1)
  • markAsRead (9-34)
apps/web/app/(org)/dashboard/caps/Caps.tsx (2)
apps/web/lib/EffectRuntime.ts (1)
  • useEffectMutation (20-20)
apps/web/lib/Rpcs.ts (1)
  • withRpc (16-17)
apps/web/app/(org)/login/form.tsx (1)
packages/database/auth/auth-options.tsx (1)
  • signIn (107-175)
apps/web/app/(org)/dashboard/caps/components/SharingDialog.tsx (1)
apps/web/actions/caps/share.ts (1)
  • shareCap (16-141)
apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx (3)
apps/web/actions/videos/download.ts (1)
  • downloadVideo (9-50)
apps/web/lib/EffectRuntime.ts (1)
  • useEffectMutation (20-20)
apps/web/lib/Rpcs.ts (1)
  • withRpc (16-17)
apps/web/app/(org)/dashboard/settings/organization/components/CustomDomainDialog/CustomDomainDialog.tsx (3)
apps/web/actions/organization/remove-domain.ts (1)
  • removeOrganizationDomain (9-59)
apps/web/actions/organization/update-domain.ts (1)
  • updateDomain (10-78)
apps/web/actions/organization/check-domain.ts (1)
  • checkOrganizationDomain (9-55)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build Desktop (aarch64-apple-darwin, macos-latest)
  • GitHub Check: Build Desktop (x86_64-pc-windows-msvc, windows-latest)
🔇 Additional comments (15)
apps/web/app/(org)/dashboard/caps/components/Folder.tsx (1)

77-87: Delete mutation: behavior and callbacks look correct

The mutation uses a clear success/error flow, refreshes the router, provides user feedback, and closes the dialog on success. Looks good.

apps/web/app/(org)/dashboard/_components/Notifications/NotificationHeader.tsx (1)

10-22: Good migration to a mutation with query invalidation

Encapsulating the mark-as-read action in a mutation and invalidating ["notifications"] is aligned with the PR objective and keeps UI consistent. Nice.

apps/web/app/(org)/login/form.tsx (2)

39-67: Solid mutation encapsulation for email sign-in

Good use of useMutation to centralize the flow, wire analytics, and handle toasts. onSuccess/onError are concise and effective.


277-287: Form submission wiring looks correct

The button derives disabled/loading from mutation state, and reset uses mutation.reset() to clear state. This keeps UI responsive and consistent.

apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx (4)

107-123: Download flow is clean and user-friendly

Good use of a mutation and ephemeral anchor to trigger downloads. Combined with toast.promise in the caller, this provides clear feedback.


219-232: Nice: toast-wrapped download with pending guard

Early-return while pending avoids duplicate requests; toast.promise surfaces errors cleanly.


336-345: Good UX for duplicate action state

Disabling while pending and inline label swap to “Duplicating...” improves clarity.


384-386: ConfirmationDialog wired correctly to mutation state

Tying loading to deleteMutation.isPending and confirming via mutate keeps the flow consistent with other components.

apps/web/app/(org)/dashboard/caps/components/SharingDialog.tsx (2)

149-160: Dialog initialization logic is clear and comprehensive

Cleanly resets selected spaces, public state, search term, and tab when opened or props change. Looks good.


351-364: Save button state wired to mutation pending

Spinner/disabled tied to updateSharing.isPending is correct and prevents duplicate submissions.

apps/web/app/(org)/dashboard/caps/components/SelectedCapsBar.tsx (1)

70-70: Simplified onClick handler - good refactor!

Removing the intermediate handleDeleteClick wrapper and directly opening the confirmation dialog is a cleaner approach.

apps/web/app/(org)/dashboard/settings/organization/components/CustomDomain.tsx (2)

29-41: Well-structured mutation implementation

The mutation setup properly handles success and error states with appropriate UI feedback and state management. Good use of React Query's mutation pattern.


43-52: Good defensive programming

The function properly checks for subscription status before proceeding with the mutation, and includes a guard clause for the organization ID.

apps/web/app/(org)/dashboard/settings/organization/components/CustomDomainDialog/CustomDomainDialog.tsx (2)

155-187: Good separation of concerns with conditional toasts

The showToasts parameter allows the mutation to be used both for automatic polling (without spamming toasts) and manual checks (with user feedback). This is a thoughtful design.


285-296: Clean refactor to mutation-based approach

The verification check is now properly handled through a mutation with appropriate loading states.

Comment on lines 132 to 152
const updateDomainMutation = useMutation({
mutationFn: async ({ domain, orgId }: { domain: string; orgId: string }) => {
if (activeOrganization?.organization.customDomain) {
await removeOrganizationDomain(orgId);
}
return await updateDomain(domain, orgId);
},
onSuccess: (data) => {
handleNext();
toast.success("Domain settings updated");
router.refresh();
if (data) {
setIsVerified(data.verified);
}
},
onError: (error) => {
toast.error(
error instanceof Error ? error.message : "Failed to update domain settings"
);
}
});
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

Sequential domain operations may cause issues

The mutation performs removeOrganizationDomain followed by updateDomain sequentially. If updateDomain fails after successful removal, the user loses their domain configuration without a replacement.

Consider making this operation atomic or adding rollback capability:

-    mutationFn: async ({ domain, orgId }: { domain: string; orgId: string }) => {
-      if (activeOrganization?.organization.customDomain) {
-        await removeOrganizationDomain(orgId);
-      }
-      return await updateDomain(domain, orgId);
-    },
+    mutationFn: async ({ domain, orgId }: { domain: string; orgId: string }) => {
+      // updateDomain should handle the removal internally in a transaction
+      // to ensure atomicity
+      return await updateDomain(domain, orgId);
+    },

Alternatively, implement proper error recovery:

     mutationFn: async ({ domain, orgId }: { domain: string; orgId: string }) => {
+      const previousDomain = activeOrganization?.organization.customDomain;
       if (activeOrganization?.organization.customDomain) {
-        await removeOrganizationDomain(orgId);
+        try {
+          await removeOrganizationDomain(orgId);
+        } catch (error) {
+          throw new Error(`Failed to remove existing domain: ${error.message}`);
+        }
+      }
+      try {
+        return await updateDomain(domain, orgId);
+      } catch (error) {
+        // Attempt to restore previous domain if update fails
+        if (previousDomain) {
+          await updateDomain(previousDomain, orgId);
+        }
+        throw error;
       }
-      return await updateDomain(domain, orgId);
     },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const updateDomainMutation = useMutation({
mutationFn: async ({ domain, orgId }: { domain: string; orgId: string }) => {
if (activeOrganization?.organization.customDomain) {
await removeOrganizationDomain(orgId);
}
return await updateDomain(domain, orgId);
},
onSuccess: (data) => {
handleNext();
toast.success("Domain settings updated");
router.refresh();
if (data) {
setIsVerified(data.verified);
}
},
onError: (error) => {
toast.error(
error instanceof Error ? error.message : "Failed to update domain settings"
);
}
});
const updateDomainMutation = useMutation({
mutationFn: async ({ domain, orgId }: { domain: string; orgId: string }) => {
const previousDomain = activeOrganization?.organization.customDomain;
if (activeOrganization?.organization.customDomain) {
try {
await removeOrganizationDomain(orgId);
} catch (error) {
throw new Error(
`Failed to remove existing domain: ${error instanceof Error ? error.message : error}`
);
}
}
try {
return await updateDomain(domain, orgId);
} catch (error) {
// Attempt to restore previous domain if update fails
if (previousDomain) {
await updateDomain(previousDomain, orgId);
}
throw error;
}
},
onSuccess: (data) => {
handleNext();
toast.success("Domain settings updated");
router.refresh();
if (data) {
setIsVerified(data.verified);
}
},
onError: (error) => {
toast.error(
error instanceof Error ? error.message : "Failed to update domain settings"
);
}
});
🤖 Prompt for AI Agents
In
apps/web/app/(org)/dashboard/settings/organization/components/CustomDomainDialog/CustomDomainDialog.tsx
around lines 132–152, the mutation removes the existing domain then calls
updateDomain, which can leave the org without a domain if updateDomain fails;
either 1) change the flow to use/to call a single backend endpoint that
atomically swaps domains (preferred), or 2) implement client-side rollback:
capture the current domain before removing, perform updateDomain first (or
create the new domain) and only remove the old on success, or if you must remove
first then on updateDomain failure call updateDomain(oldDomain, orgId) to
restore the previous value and surface a clear error; ensure to handle and
surface errors from the rollback attempt and update UI state (isVerified) only
on confirmed success.

Copy link
Contributor

@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: 3

🔭 Outside diff range comments (1)
apps/web/app/(org)/dashboard/caps/components/SharingDialog.tsx (1)

80-143: Avoid stale state in onSuccess: use mutation variables to compute diffs and toasts

onSuccess closes over selectedSpaces/publicToggle at callback execution time, which can drift from the values sent to the server if the user toggles while the request is in flight. Use the variables argument from React Query to compute toasts and propagate updates, ensuring consistency with the actually-saved payload.

Apply:

-  const updateSharing = useMutation({
-    mutationFn: async ({
+  const updateSharing = useMutation<
+    void,
+    Error,
+    { capId: string; spaceIds: string[]; public: boolean }
+  >({
+    mutationFn: async ({
       capId,
       spaceIds,
       public: isPublic,
-    }: {
-      capId: string;
-      spaceIds: string[];
-      public: boolean;
-    }) => {
+    }) => {
       const result = await shareCap({ capId, spaceIds, public: isPublic });
 
       if (!result.success) {
         throw new Error(result.error || "Failed to update sharing settings");
       }
     },
-    onSuccess: () => {
-      const newSelectedSpaces = Array.from(selectedSpaces);
+    onSuccess: (_data, variables) => {
+      const newSelectedSpaces = variables.spaceIds;
       const initialSpaces = Array.from(initialSelectedSpaces);
 
       const addedSpaceIds = newSelectedSpaces.filter(
         (id) => !initialSpaces.includes(id)
       );
       const removedSpaceIds = initialSpaces.filter(
         (id) => !newSelectedSpaces.includes(id)
       );
 
-      const publicChanged = publicToggle !== initialPublicState;
+      const publicChanged = variables.public !== initialPublicState;
 
       const getSpaceName = (id: string) => {
         const space = spacesData?.find((space) => space.id === id);
         return space?.name || `Space ${id}`;
       };
 
       if (
         publicChanged &&
         addedSpaceIds.length === 0 &&
         removedSpaceIds.length === 0
       ) {
         toast.success(
-          publicToggle ? "Video is now public" : "Video is now private"
+          variables.public ? "Video is now public" : "Video is now private"
         );
       } else if (
         addedSpaceIds.length === 1 &&
         removedSpaceIds.length === 0 &&
         !publicChanged
       ) {
         toast.success(`Shared to ${getSpaceName(addedSpaceIds[0] as string)}`);
       } else if (
         removedSpaceIds.length === 1 &&
         addedSpaceIds.length === 0 &&
         !publicChanged
       ) {
         toast.success(
           `Unshared from ${getSpaceName(removedSpaceIds[0] as string)}`
         );
       } else if (
         addedSpaceIds.length > 0 &&
         removedSpaceIds.length === 0 &&
         !publicChanged
       ) {
         toast.success(`Shared to ${addedSpaceIds.length} spaces`);
       } else if (
         removedSpaceIds.length > 0 &&
         addedSpaceIds.length === 0 &&
         !publicChanged
       ) {
         toast.success(`Unshared from ${removedSpaceIds.length} spaces`);
       } else if (
         addedSpaceIds.length > 0 ||
         removedSpaceIds.length > 0 ||
         publicChanged
       ) {
         toast.success(`Sharing settings updated`);
       } else {
         toast.info("No changes to sharing settings");
       }
-      onSharingUpdated(newSelectedSpaces);
+      onSharingUpdated(newSelectedSpaces);
       onClose();
     },
♻️ Duplicate comments (1)
apps/web/app/(org)/dashboard/caps/components/SharingDialog.tsx (1)

64-79: Previous feedback addressed: error handling is now in mutationFn; onSuccess no longer throws

This resolves the earlier review direction. The mutation rejects on failure and keeps onSuccess side-effect-only. Good use of isPending for UI state.

Also applies to: 80-147

🧹 Nitpick comments (4)
apps/web/app/(org)/dashboard/caps/components/SharingDialog.tsx (4)

80-143: Prefer cache invalidation over manual parent callbacks to align with React Query

Given the PR objective to centralize state with React Query, consider replacing onSharingUpdated with query invalidations (e.g., invalidate “caps” and “cap by id” queries) so the UI derives from the cache instead of bespoke callbacks.

If helpful, I can propose a concrete invalidateQueries snippet once you confirm the query keys used for caps and cap detail.


280-283: Disable the public switch while saving

Avoids public toggle changes mid-request which could desync the success toasts and updated state.

   <Switch
     checked={publicToggle}
-    onCheckedChange={setPublicToggle}
+    onCheckedChange={setPublicToggle}
+    disabled={updateSharing.isPending}
   />

450-453: Conflicting background classes override the green check indicator

bg-green-500 and bg-gray-4 are both applied; the latter overrides the green. Remove the gray background to keep the check badge green.

-          className="flex absolute -top-2 -right-2 z-10 justify-center items-center bg-green-500 rounded-full bg-gray-4 size-4"
+          className="flex absolute -top-2 -right-2 z-10 justify-center items-center bg-green-500 rounded-full size-4"

236-245: Don’t attach onClick handler to the active tab

The UI shows a not-allowed cursor for the active tab but still wires onClick. No-op clicks improve semantics and avoid unnecessary state updates.

-            <div
+            <div
               key={tab}
               className={clsx(
                 "flex relative flex-1 justify-center items-center w/full min-w-0 text-sm font-medium transition-colors",
                 activeTab === tab
                   ? "cursor-not-allowed bg-gray-3"
                   : "cursor-pointer"
               )}
-              onClick={() => setActiveTab(tab)}
+              onClick={activeTab === tab ? undefined : () => setActiveTab(tab)}
             >
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 61594ae and be052c0.

📒 Files selected for processing (1)
  • apps/web/app/(org)/dashboard/caps/components/SharingDialog.tsx (8 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
apps/web/app/(org)/dashboard/caps/components/SharingDialog.tsx (1)
apps/web/actions/caps/share.ts (1)
  • shareCap (16-141)

import { motion } from "framer-motion";
import { Check, Search, Globe2 } from "lucide-react";
import Image from "next/image";
import { useCallback, useEffect, useState } from "react";
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Import useMemo to support organization-level memoization

Needed by the selectedOrgIds computation.

-import { useCallback, useEffect, useState } from "react";
+import { useCallback, useEffect, useMemo, useState } from "react";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { useCallback, useEffect, useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
🤖 Prompt for AI Agents
In apps/web/app/(org)/dashboard/caps/components/SharingDialog.tsx around line
19, the code uses a memoized computation for selectedOrgIds but did not import
useMemo; add useMemo to the React import list (import { useCallback, useEffect,
useState, useMemo } from "react") and wrap the selectedOrgIds computation with
useMemo so the list is only recalculated when its dependencies change.

Comment on lines 149 to 170
const sharedSpaceIds = new Set(sharedSpaces?.map((space) => space.id) || []);

useEffect(() => {
if (isOpen && sharedSpaces) {
const spaceIds = new Set(sharedSpaces.map((space) => space.id));
setSelectedSpaces(spaceIds);
setInitialSelectedSpaces(spaceIds);
setPublicToggle(isPublic);
setInitialPublicState(isPublic);
setSearchTerm("");
setActiveTab(tabs[0]);
}
}, [isOpen, sharedSpaces, isPublic]);

const isSpaceSharedViaOrganization = useCallback(
(spaceId: string) => {
const space = spacesData?.find((s) => s.id === spaceId);
if (!space) return false;
return sharedSpaceIds.has(space.id);
},
[spacesData, sharedSpaceIds]
);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Incorrect ‘shared via organization’ detection and stale basis: use selected org entries, not initial sharedSpaces

The current logic returns sharedSpaceIds.has(space.id), which simply marks directly shared spaces, not spaces implicitly shared via an organization. It also never updates when the user toggles org entries during this dialog (it’s based on props, not current selection).

Refactor to:

  • Derive the set of selected organization IDs from selectedSpaces.
  • Consider a space “shared via organization” if its organizationId is in that set and it is not itself the org entry.

Apply:

-  const sharedSpaceIds = new Set(sharedSpaces?.map((space) => space.id) || []);
+  // Track which organizations are selected (org entry is a space where id === organizationId and primary === true)
+  const selectedOrgIds = useMemo(() => {
+    const orgs = new Set<string>();
+    if (!spacesData) return orgs;
+    for (const id of selectedSpaces) {
+      const s = spacesData.find((sp) => sp.id === id);
+      if (s && s.id === s.organizationId && s.primary === true) {
+        orgs.add(s.organizationId);
+      }
+    }
+    return orgs;
+  }, [selectedSpaces, spacesData]);
@@
-  const isSpaceSharedViaOrganization = useCallback(
-    (spaceId: string) => {
-      const space = spacesData?.find((s) => s.id === spaceId);
-      if (!space) return false;
-      return sharedSpaceIds.has(space.id);
-    },
-    [spacesData, sharedSpaceIds]
-  );
+  const isSpaceSharedViaOrganization = useCallback(
+    (spaceId: string) => {
+      const space = spacesData?.find((s) => s.id === spaceId);
+      if (!space) return false;
+      return selectedOrgIds.has(space.organizationId) && space.id !== space.organizationId;
+    },
+    [spacesData, selectedOrgIds]
+  );

Note: This requires importing useMemo (see separate import change).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const sharedSpaceIds = new Set(sharedSpaces?.map((space) => space.id) || []);
useEffect(() => {
if (isOpen && sharedSpaces) {
const spaceIds = new Set(sharedSpaces.map((space) => space.id));
setSelectedSpaces(spaceIds);
setInitialSelectedSpaces(spaceIds);
setPublicToggle(isPublic);
setInitialPublicState(isPublic);
setSearchTerm("");
setActiveTab(tabs[0]);
}
}, [isOpen, sharedSpaces, isPublic]);
const isSpaceSharedViaOrganization = useCallback(
(spaceId: string) => {
const space = spacesData?.find((s) => s.id === spaceId);
if (!space) return false;
return sharedSpaceIds.has(space.id);
},
[spacesData, sharedSpaceIds]
);
// Track which organizations are selected (org entry is a space where id === organizationId and primary === true)
const selectedOrgIds = useMemo(() => {
const orgs = new Set<string>();
if (!spacesData) return orgs;
for (const id of selectedSpaces) {
const s = spacesData.find((sp) => sp.id === id);
if (s && s.id === s.organizationId && s.primary === true) {
orgs.add(s.organizationId);
}
}
return orgs;
}, [selectedSpaces, spacesData]);
useEffect(() => {
if (isOpen && sharedSpaces) {
const spaceIds = new Set(sharedSpaces.map((space) => space.id));
setSelectedSpaces(spaceIds);
setInitialSelectedSpaces(spaceIds);
setPublicToggle(isPublic);
setInitialPublicState(isPublic);
setSearchTerm("");
setActiveTab(tabs[0]);
}
}, [isOpen, sharedSpaces, isPublic]);
const isSpaceSharedViaOrganization = useCallback(
(spaceId: string) => {
const space = spacesData?.find((s) => s.id === spaceId);
if (!space) return false;
return selectedOrgIds.has(space.organizationId) && space.id !== space.organizationId;
},
[spacesData, selectedOrgIds]
);
🤖 Prompt for AI Agents
In apps/web/app/(org)/dashboard/caps/components/SharingDialog.tsx around lines
149 to 170, the isSpaceSharedViaOrganization check currently uses the stale
sharedSpaceIds derived from props and only detects directly shared spaces;
change it to compute the set of selected organization IDs from the current
selectedSpaces (use useMemo and add it to imports), and then consider a space
"shared via organization" when its organizationId is in that selected-org-id set
and the space itself is not the org entry (i.e., exclude when space.id ===
orgId); update the hook dependencies to use selectedSpaces (or the memo) instead
of sharedSpaces/props so the result updates as the user toggles org entries.

Comment on lines 172 to 182
const handleToggleSpace = (spaceId: string) => {
setSelectedSpaces((prev) => {
const newSet = new Set(prev);
if (newSet.has(spaceId)) {
newSet.delete(spaceId);
} else {
newSet.add(spaceId);
}
return newSet;
});
};
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Prevent edits while saving to keep UI state consistent with the in-flight request

Guard toggling when the mutation is pending to avoid divergence between server-persisted data and the local selection used in toasts and callbacks.

-  const handleToggleSpace = (spaceId: string) => {
-    setSelectedSpaces((prev) => {
+  const handleToggleSpace = (spaceId: string) => {
+    if (updateSharing.isPending) return;
+    setSelectedSpaces((prev) => {
       const newSet = new Set(prev);
       if (newSet.has(spaceId)) {
         newSet.delete(spaceId);
       } else {
         newSet.add(spaceId);
       }
       return newSet;
     });
   };

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/web/app/(org)/dashboard/caps/components/SharingDialog.tsx around lines
172 to 182, the handleToggleSpace function allows toggling while a save mutation
is in-flight which can cause UI/server state divergence; update the function to
early-return when the relevant saving flag (e.g., mutation.isLoading, isSaving
or similar prop/state used for the sharing mutation) is true so toggles are
ignored during pending saves, and optionally show a disabled UI affordance or a
toast to indicate the action is blocked while saving.

Copy link
Contributor

@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: 0

♻️ Duplicate comments (4)
apps/web/app/(org)/dashboard/caps/components/SharingDialog.tsx (2)

172-182: Prevent toggles while save is in-flight

Allowing edits while updateSharing is pending can desync UI from persisted state and break your toasts/diff logic.

- const handleToggleSpace = (spaceId: string) => {
-   setSelectedSpaces((prev) => {
+ const handleToggleSpace = (spaceId: string) => {
+   if (updateSharing.isPending) return;
+   setSelectedSpaces((prev) => {
      const newSet = new Set(prev);
      if (newSet.has(spaceId)) {
        newSet.delete(spaceId);
      } else {
        newSet.add(spaceId);
      }
      return newSet;
    });
  };

149-170: Fix “shared via organization” detection; current logic is stale and incorrect
The current check uses sharedSpaceIds derived from props (initial shares) and only detects direct shares. It fails to reflect user changes during the dialog and doesn’t capture org-level implicit sharing.

Refactor to compute selected organization entries from current selection and base the “via org” flag on that:

- const sharedSpaceIds = new Set(sharedSpaces?.map((space) => space.id) || []);
+ // Track which organization entries are currently selected.
+ // An org entry is represented by a space with id === organizationId and primary === true.
+ const selectedOrgIds = useMemo(() => {
+   const orgs = new Set<string>();
+   if (!spacesData) return orgs;
+   for (const id of selectedSpaces) {
+     const space = spacesData.find((sp) => sp.id === id);
+     if (space && space.id === space.organizationId && space.primary === true) {
+       orgs.add(space.organizationId);
+     }
+   }
+   return orgs;
+ }, [selectedSpaces, spacesData]);
@@
- const isSpaceSharedViaOrganization = useCallback(
-   (spaceId: string) => {
-     const space = spacesData?.find((s) => s.id === spaceId);
-     if (!space) return false;
-     return sharedSpaceIds.has(space.id);
-   },
-   [spacesData, sharedSpaceIds],
- );
+ const isSpaceSharedViaOrganization = useCallback(
+   (spaceId: string) => {
+     const space = spacesData?.find((s) => s.id === spaceId);
+     if (!space) return false;
+     // Consider it shared via org when its org is selected and it isn't the org-entry itself
+     return selectedOrgIds.has(space.organizationId) && space.id !== space.organizationId;
+   },
+   [spacesData, selectedOrgIds],
+ );

Additionally (outside this hunk), ensure useMemo is imported:

// at import line with React hooks
import { useCallback, useEffect, useMemo, useState } from "react";
apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx (1)

136-144: Refresh list after duplicate so new item appears immediately

After duplication, the caps list won’t reflect the new item until the page revalidates. Trigger a refresh on success.

 const duplicateMutation = useEffectMutation({
   mutationFn: () => withRpc((r) => r.VideoDuplicate(cap.id)),
   onSuccess: () => {
     toast.success("Cap duplicated successfully");
+    router.refresh();
   },
   onError: () => {
     toast.error("Failed to duplicate cap");
   },
 });
apps/web/app/(org)/dashboard/settings/organization/components/CustomDomainDialog/CustomDomainDialog.tsx (1)

144-158: Avoid “remove then update” split; make it atomic or add rollback

Removing the existing domain and then updating can leave the org with no domain if the update fails. Prefer a single backend operation that swaps atomically. If you must keep it client-driven, implement a best-effort rollback.

Apply a client-side rollback until a server-side atomic swap is available:

 const updateDomainMutation = useMutation({
   mutationFn: async ({
     domain,
     orgId,
   }: {
     domain: string;
     orgId: string;
   }) => {
-    if (activeOrganization?.organization.customDomain) {
-      await removeOrganizationDomain(orgId);
-    }
-    return await updateDomain(domain, orgId);
+    const previousDomain = activeOrganization?.organization.customDomain;
+    if (previousDomain) {
+      try {
+        await removeOrganizationDomain(orgId);
+      } catch (err) {
+        throw new Error(
+          `Failed to remove existing domain: ${
+            err instanceof Error ? err.message : String(err)
+          }`,
+        );
+      }
+    }
+    try {
+      return await updateDomain(domain, orgId);
+    } catch (err) {
+      // Best-effort restore of previous domain
+      if (previousDomain) {
+        try {
+          await updateDomain(previousDomain, orgId);
+        } catch {
+          // ignore restore failure, original error will be thrown
+        }
+      }
+      throw err instanceof Error ? err : new Error("Failed to update domain");
+    }
   },

Longer-term: move this logic to a server action (transaction/compensating steps) to guarantee atomicity.

🧹 Nitpick comments (4)
apps/web/app/(org)/dashboard/settings/organization/components/CustomDomain.tsx (1)

32-45: Consolidate dialog closing into onSettled and surface server error messages

Reduce duplication by moving setConfirmOpen(false) to onSettled, and show actionable error messages from the server instead of a generic one.

Apply:

- const removeDomainMutation = useMutation({
+ const removeDomainMutation = useMutation({
   mutationFn: (organizationId: string) =>
     removeOrganizationDomain(organizationId),
-  onSuccess: () => {
+  onSuccess: () => {
     setIsVerified(false);
     toast.success("Custom domain removed");
     router.refresh();
-    setConfirmOpen(false);
-  },
-  onError: () => {
-    toast.error("Failed to remove domain");
-    setConfirmOpen(false);
-  },
+  },
+  onError: (error) => {
+    toast.error(
+      error instanceof Error ? error.message : "Failed to remove domain",
+    );
+  },
+  onSettled: () => {
+    setConfirmOpen(false);
+  },
 });
apps/web/app/(org)/dashboard/caps/components/SharingDialog.tsx (1)

144-147: Revert local state on error to avoid UI/server divergence

On failure, selection may not match the server but the dialog keeps showing local changes. Resetting selection and publicToggle to initial values keeps UI consistent.

- onError: () => {
-   toast.error("Failed to update sharing settings");
- },
+ onError: () => {
+   toast.error("Failed to update sharing settings");
+   setSelectedSpaces(new Set(initialSelectedSpaces));
+   setPublicToggle(initialPublicState);
+ },
apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx (1)

124-134: Surface delete errors to users

Currently logs to console only. Add a toast to inform users of failures.

 const deleteMutation = useMutation({
   mutationFn: async () => {
     await onDelete?.();
   },
-  onError: (error) => {
-    console.error("Error deleting cap:", error);
-  },
+  onError: (error) => {
+    console.error("Error deleting cap:", error);
+    toast.error("Failed to delete cap");
+  },
   onSettled: () => {
     setConfirmOpen(false);
   },
 });
apps/web/app/(org)/dashboard/settings/organization/components/CustomDomainDialog/CustomDomainDialog.tsx (1)

308-312: Guard against missing organization ID before mutating

activeOrganization?.organization.id is asserted as string. If undefined, this will pass a wrong value into the mutation.

Consider a defensive guard:

- updateDomainMutation.mutate({
-   domain: cleanedDomain,
-   orgId: activeOrganization?.organization.id as string,
- });
+ const orgId = activeOrganization?.organization.id;
+ if (!orgId) {
+   toast.error("No active organization found");
+   return;
+ }
+ updateDomainMutation.mutate({ domain: cleanedDomain, orgId });
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between be052c0 and 21bbfe9.

📒 Files selected for processing (11)
  • .eslintrc.ts (0 hunks)
  • apps/web/app/(org)/dashboard/_components/Notifications/NotificationHeader.tsx (1 hunks)
  • apps/web/app/(org)/dashboard/caps/Caps.tsx (1 hunks)
  • apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx (8 hunks)
  • apps/web/app/(org)/dashboard/caps/components/Folder.tsx (3 hunks)
  • apps/web/app/(org)/dashboard/caps/components/SelectedCapsBar.tsx (1 hunks)
  • apps/web/app/(org)/dashboard/caps/components/SharingDialog.tsx (4 hunks)
  • apps/web/app/(org)/dashboard/folder/[id]/components/FolderVideosSection.tsx (1 hunks)
  • apps/web/app/(org)/dashboard/settings/organization/components/CustomDomain.tsx (4 hunks)
  • apps/web/app/(org)/dashboard/settings/organization/components/CustomDomainDialog/CustomDomainDialog.tsx (8 hunks)
  • apps/web/app/(org)/login/form.tsx (5 hunks)
💤 Files with no reviewable changes (1)
  • .eslintrc.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • apps/web/app/(org)/dashboard/caps/components/SelectedCapsBar.tsx
  • apps/web/app/(org)/dashboard/folder/[id]/components/FolderVideosSection.tsx
  • apps/web/app/(org)/dashboard/_components/Notifications/NotificationHeader.tsx
  • apps/web/app/(org)/dashboard/caps/Caps.tsx
  • apps/web/app/(org)/dashboard/caps/components/Folder.tsx
  • apps/web/app/(org)/login/form.tsx
🧰 Additional context used
🧬 Code Graph Analysis (4)
apps/web/app/(org)/dashboard/caps/components/SharingDialog.tsx (1)
apps/web/actions/caps/share.ts (1)
  • shareCap (23-157)
apps/web/app/(org)/dashboard/settings/organization/components/CustomDomain.tsx (1)
apps/web/actions/organization/remove-domain.ts (1)
  • removeOrganizationDomain (9-59)
apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx (3)
apps/web/actions/videos/download.ts (1)
  • downloadVideo (9-50)
apps/web/lib/EffectRuntime.ts (1)
  • useEffectMutation (19-19)
apps/web/lib/Rpcs.ts (1)
  • withRpc (16-17)
apps/web/app/(org)/dashboard/settings/organization/components/CustomDomainDialog/CustomDomainDialog.tsx (3)
apps/web/actions/organization/remove-domain.ts (1)
  • removeOrganizationDomain (9-59)
apps/web/actions/organization/update-domain.ts (1)
  • updateDomain (10-78)
apps/web/actions/organization/check-domain.ts (1)
  • checkOrganizationDomain (9-55)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build Desktop (x86_64-pc-windows-msvc, windows-latest)
  • GitHub Check: Build Desktop (aarch64-apple-darwin, macos-latest)
🔇 Additional comments (7)
apps/web/app/(org)/dashboard/settings/organization/components/CustomDomain.tsx (2)

47-55: Subscription gate for removal may be unintended; backend doesn’t enforce it

Frontend blocks removal when not subscribed, but removeOrganizationDomain (apps/web/actions/organization/remove-domain.ts) does not check subscription. This can prevent users from removing a domain they no longer want/need when unsubscribed.

  • Confirm product intent. If removal should be allowed regardless of subscription, drop the isSubscribed check here.
  • If removal must require subscription, mirror that policy server-side to keep behavior consistent and secure.

77-77: Good: UI bound to mutation state

ConfirmationDialog and the primary Button reliably reflect mutation.isPending, preventing duplicate submissions and keeping UX consistent.

Also applies to: 148-150

apps/web/app/(org)/dashboard/caps/components/SharingDialog.tsx (2)

64-80: Good: mutation throws on failure; onSuccess used for side-effects only

Correctly moves failure handling into mutationFn and keeps onSuccess strictly for side-effects. This aligns with React Query best practices.


353-366: Good: Disabled and spinner wired to mutation state

Save button correctly reflects updateSharing.isPending and prevents duplicate submissions.

apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx (1)

219-232: Good: Download flow uses mutation + toast.promise and guards re-entry

  • Prevents double-trigger via isPending guard.
  • Surfaces progress/success/failure via toast.
  • Safely generates and clicks a temporary anchor for download.

Also applies to: 107-123

apps/web/app/(org)/dashboard/settings/organization/components/CustomDomainDialog/CustomDomainDialog.tsx (2)

194-202: Confirm pollInterval lifecycle

You clear pollInterval.current on verification, but this ref isn’t set anywhere in this component. If polling is intended, ensure it is initialized (and cleared on unmount) or remove the cleanup.


381-383: Good: Buttons are correctly bound to mutation state

  • Domain step Next: spinner/disabled driven by updateDomainMutation.isPending.
  • Verify step Check Status: disabled and shows spinner while pending.

Also applies to: 440-443, 416-431

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.

3 participants