Skip to content

Conversation

@MohamedBassem
Copy link
Collaborator

No description provided.

claude and others added 2 commits December 5, 2025 08:24
Add quota validation before bookmark import to prevent users from
exceeding their bookmark limits. The implementation includes:

- New QuotaService.canImportBookmarks() method to check if user can import N bookmarks
- New tRPC checkImportQuota procedure for client-side quota validation
- Updated useBookmarkImport hook to parse files and check quota before import
- Added error banner in ImportExport component to display quota errors
- Optimized file parsing to avoid reading the file twice

The quota check displays remaining bookmarks and provides clear error
messages when the import would exceed the user's quota.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 8, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds quota validation to bookmark imports: a parse step counts bookmarks, checks against user quota, surfaces a quotaError state from useBookmarkImport, exposes parseImportFile from shared imports, and shows a destructive, dismissible "Import Quota Exceeded" alert in the ImportExport UI when limits are exceeded.

Changes

Cohort / File(s) Summary
Bookmark import hook
apps/web/lib/hooks/useBookmarkImport.ts
Adds parsing of import files to count bookmarks, checks upcoming import against user quota, introduces quotaError and clearQuotaError state, reuses parsed data during import to avoid double-parse, and extends public hook return values.
Import UI
apps/web/components/settings/ImportExport.tsx
Reads quotaError from useBookmarkImport, renders a destructive Alert with icon and title "Import Quota Exceeded" showing the quota message, and allows dismissing the error. No changes to core upload logic.
Shared parsers export
packages/shared/import-export/index.ts
Re-exports parseImportFile from ./parsers to allow counting/parsing imports in the hook without duplicating parser code.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Review parsing logic and correctness of bookmark counting and edge cases (empty/malformed files).
  • Verify quota check conditions (unlimited quota handling, thresholds) and mutation gating.
  • Confirm exported parseImportFile signature matches hook usage and no consumers break from the hook return change.

Pre-merge checks

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Description check ❓ Inconclusive No pull request description was provided by the author, making it impossible to evaluate the description content. Add a pull request description explaining the motivation, changes, and any relevant context for this quota-checking feature.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix: check import quota before importing bookmarks' directly and clearly summarizes the main change: adding quota validation before the bookmark import process.

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

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 72 to 76
const remaining =
quotaUsage.bookmarks.quota! - quotaUsage.bookmarks.used;

if (remaining < bookmarkCount) {
const errorMsg = `Cannot import ${bookmarkCount} bookmarks. You have ${remaining} bookmark${remaining === 1 ? "" : "s"} remaining in your quota of ${quotaUsage.bookmarks.quota}.`;

Choose a reason for hiding this comment

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

P1 Badge Quota check blocks imports with mostly existing bookmarks

The new quota guard compares the raw number of parsed rows to the remaining quota and throws when bookmarkCount exceeds remaining. This treats every entry in the file as consuming quota even though the import pipeline later deduplicates and counts alreadyExisted items separately, so a user near their limit is now prevented from importing a file that mostly contains bookmarks already stored (e.g., 4 existing + 1 new with 1 slot left will be rejected). This misestimates quota usage and unnecessarily blocks legitimate imports.

Useful? React with 👍 / 👎.

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: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b6c2dad and 06a5676.

📒 Files selected for processing (3)
  • apps/web/components/settings/ImportExport.tsx (3 hunks)
  • apps/web/lib/hooks/useBookmarkImport.ts (5 hunks)
  • packages/shared/import-export/index.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use TypeScript for type safety in all source files

Files:

  • packages/shared/import-export/index.ts
  • apps/web/components/settings/ImportExport.tsx
  • apps/web/lib/hooks/useBookmarkImport.ts
**/*.{ts,tsx,js,jsx,json,css,md}

📄 CodeRabbit inference engine (AGENTS.md)

Format code using Prettier according to project standards

Files:

  • packages/shared/import-export/index.ts
  • apps/web/components/settings/ImportExport.tsx
  • apps/web/lib/hooks/useBookmarkImport.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Lint code using oxlint and fix issues with pnpm lint:fix

Files:

  • packages/shared/import-export/index.ts
  • apps/web/components/settings/ImportExport.tsx
  • apps/web/lib/hooks/useBookmarkImport.ts
packages/shared/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Organize shared code and types in the packages/shared directory for use across packages

Files:

  • packages/shared/import-export/index.ts
apps/web/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/**/*.{ts,tsx}: Use Tailwind CSS for styling in the web application
Use Next.js for building the main web application

Files:

  • apps/web/components/settings/ImportExport.tsx
  • apps/web/lib/hooks/useBookmarkImport.ts
🧬 Code graph analysis (2)
apps/web/components/settings/ImportExport.tsx (2)
apps/web/lib/hooks/useBookmarkImport.ts (1)
  • useBookmarkImport (35-202)
apps/web/components/ui/alert.tsx (3)
  • Alert (60-60)
  • AlertTitle (60-60)
  • AlertDescription (60-60)
apps/web/lib/hooks/useBookmarkImport.ts (2)
packages/shared/import-export/index.ts (1)
  • parseImportFile (4-4)
packages/shared/import-export/parsers.ts (1)
  • parseImportFile (336-365)
⏰ 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: tests
  • GitHub Check: typecheck
🔇 Additional comments (7)
packages/shared/import-export/index.ts (1)

4-4: LGTM!

Clean re-export that enables the quota-aware import flow by exposing the parser function for pre-parsing files before quota validation.

apps/web/components/settings/ImportExport.tsx (2)

4-4: LGTM!

Appropriate imports for the quota error alert UI components.

Also applies to: 19-19


135-136: LGTM!

Correctly consumes the new quota error state from the hook.

apps/web/lib/hooks/useBookmarkImport.ts (4)

16-16: LGTM!

Appropriate imports for TRPC client access and file parsing functionality.

Also applies to: 21-21


41-41: LGTM!

Correctly initializes quota error state and TRPC utilities for quota validation.

Also applies to: 43-43


197-198: LGTM!

Correctly exposes the new quota error state and clear function in the hook's public API.


156-161: LGTM! Clever optimization to avoid re-parsing the file by providing the already-parsed bookmarks via a custom parser. This prevents redundant parsing work.

Comment on lines 140 to 154
{quotaError && (
<Alert variant="destructive" className="relative">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Import Quota Exceeded</AlertTitle>
<AlertDescription>{quotaError}</AlertDescription>
<Button
variant="ghost"
size="sm"
className="absolute right-2 top-2 h-6 w-6 p-0"
onClick={clearQuotaError}
>
<X className="h-4 w-4" />
</Button>
</Alert>
)}
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 | 🟡 Minor

Hardcoded English text breaks i18n consistency.

The Alert title "Import Quota Exceeded" is hardcoded in English, while the rest of the component uses t() for internationalization.

Apply this diff to make the text translatable:

-          <AlertTitle>Import Quota Exceeded</AlertTitle>
+          <AlertTitle>{t("settings.import.quota_exceeded_title")}</AlertTitle>

Additionally, the quotaError message from the hook is also in English. Consider whether the error message construction should also use i18n for consistency across the application.


Optional: Consider relative positioning for the dismiss button.

The absolute positioning at right-2 top-2 might overlap with the alert content on smaller screens or with longer error messages. Consider using a flex layout instead for more robust responsive behavior.

Comment on lines 58 to 81
// Clear any previous quota error
setQuotaError(null);

// First, parse the file to count bookmarks
const textContent = await file.text();
const parsedBookmarks = parseImportFile(source, textContent);
const bookmarkCount = parsedBookmarks.length;

// Check quota before proceeding
if (bookmarkCount > 0) {
const quotaUsage =
await apiUtils.client.subscriptions.getQuotaUsage.query();

if (!quotaUsage.bookmarks.unlimited) {
const remaining =
quotaUsage.bookmarks.quota! - quotaUsage.bookmarks.used;

if (remaining < bookmarkCount) {
const errorMsg = `Cannot import ${bookmarkCount} bookmarks. You have ${remaining} bookmark${remaining === 1 ? "" : "s"} remaining in your quota of ${quotaUsage.bookmarks.quota}.`;
setQuotaError(errorMsg);
throw new Error(errorMsg);
}
}
}
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 | 🟠 Major

Duplicate error messaging: both Alert and toast are shown.

When quota is exceeded, the error is both stored in state (line 77) and thrown (line 78). The thrown error triggers onError (line 186), which displays a toast. This results in the user seeing both the persistent Alert (rendered in ImportExport.tsx) and a temporary toast with the same message.

Consider one of these approaches:

Option 1: Don't throw after setting quotaError (recommended)

           if (remaining < bookmarkCount) {
             const errorMsg = `Cannot import ${bookmarkCount} bookmarks. You have ${remaining} bookmark${remaining === 1 ? "" : "s"} remaining in your quota of ${quotaUsage.bookmarks.quota}.`;
             setQuotaError(errorMsg);
-            throw new Error(errorMsg);
+            return { counts: { total: 0, successes: 0, failures: 0, alreadyExisted: 0 } };
           }

Option 2: Only throw without setting state

           if (remaining < bookmarkCount) {
             const errorMsg = `Cannot import ${bookmarkCount} bookmarks. You have ${remaining} bookmark${remaining === 1 ? "" : "s"} remaining in your quota of ${quotaUsage.bookmarks.quota}.`;
-            setQuotaError(errorMsg);
             throw new Error(errorMsg);
           }

Then remove the Alert UI since toast will handle it.


Non-null assertion could cause runtime error.

Line 73 uses quotaUsage.bookmarks.quota! assuming quota is always defined when unlimited is false. Verify this is guaranteed by the API contract.

Consider adding a runtime check:

         if (!quotaUsage.bookmarks.unlimited) {
+          if (quotaUsage.bookmarks.quota === null || quotaUsage.bookmarks.quota === undefined) {
+            throw new Error("Quota information unavailable");
+          }
           const remaining =
             quotaUsage.bookmarks.quota! - quotaUsage.bookmarks.used;

Or use nullish coalescing if there's a reasonable default:

-          const remaining = quotaUsage.bookmarks.quota! - quotaUsage.bookmarks.used;
+          const remaining = (quotaUsage.bookmarks.quota ?? 0) - quotaUsage.bookmarks.used;

Consider adding loading state during file parsing.

Lines 62-63 parse the entire file synchronously (from the user's perspective). For large import files, this could cause the UI to appear unresponsive. Consider showing a loading indicator or moving this to a Web Worker.

📝 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
// Clear any previous quota error
setQuotaError(null);
// First, parse the file to count bookmarks
const textContent = await file.text();
const parsedBookmarks = parseImportFile(source, textContent);
const bookmarkCount = parsedBookmarks.length;
// Check quota before proceeding
if (bookmarkCount > 0) {
const quotaUsage =
await apiUtils.client.subscriptions.getQuotaUsage.query();
if (!quotaUsage.bookmarks.unlimited) {
const remaining =
quotaUsage.bookmarks.quota! - quotaUsage.bookmarks.used;
if (remaining < bookmarkCount) {
const errorMsg = `Cannot import ${bookmarkCount} bookmarks. You have ${remaining} bookmark${remaining === 1 ? "" : "s"} remaining in your quota of ${quotaUsage.bookmarks.quota}.`;
setQuotaError(errorMsg);
throw new Error(errorMsg);
}
}
}
// Clear any previous quota error
setQuotaError(null);
// First, parse the file to count bookmarks
const textContent = await file.text();
const parsedBookmarks = parseImportFile(source, textContent);
const bookmarkCount = parsedBookmarks.length;
// Check quota before proceeding
if (bookmarkCount > 0) {
const quotaUsage =
await apiUtils.client.subscriptions.getQuotaUsage.query();
if (!quotaUsage.bookmarks.unlimited) {
const remaining =
quotaUsage.bookmarks.quota! - quotaUsage.bookmarks.used;
if (remaining < bookmarkCount) {
const errorMsg = `Cannot import ${bookmarkCount} bookmarks. You have ${remaining} bookmark${remaining === 1 ? "" : "s"} remaining in your quota of ${quotaUsage.bookmarks.quota}.`;
setQuotaError(errorMsg);
return { counts: { total: 0, successes: 0, failures: 0, alreadyExisted: 0 } };
}
}
}
🤖 Prompt for AI Agents
In apps/web/lib/hooks/useBookmarkImport.ts around lines 58 to 81, when quota is
exceeded the code both sets quotaError state and throws an Error which leads to
duplicate UI messages (persistent Alert + toast); remove the throw after calling
setQuotaError so only the Alert is shown (or alternatively remove setting state
and only throw if you prefer toast-only behavior), and replace the non-null
assertion on quota (quota!) with a runtime check that handles missing quota
(e.g., bail out or treat as zero/unlimited based on API contract) to avoid
potential runtime errors; optionally, add a brief loading state while parsing
the file or move parsing to a worker to keep the UI responsive for large
imports.

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: 1

♻️ Duplicate comments (4)
apps/web/lib/hooks/useBookmarkImport.ts (3)

61-64: Consider showing a loading state during file parsing.

For large import files, await file.text() and parseImportFile() can take noticeable time and make the UI appear unresponsive. Consider adding a loading indicator or moving parsing to a Web Worker.


78-81: Remove the throw to avoid duplicate error messages.

Setting quotaError (line 80) displays a persistent Alert in the UI, while throwing the error (line 81) triggers onError (line 189), which shows a toast. This results in users seeing the same error message twice.

Apply this diff to show only the Alert:

           if (remaining < bookmarkCount) {
             const errorMsg = `Cannot import ${bookmarkCount} bookmarks. You have ${remaining} bookmark${remaining === 1 ? "" : "s"} remaining in your quota of ${quotaUsage.bookmarks.quota}.`;
             setQuotaError(errorMsg);
-            throw new Error(errorMsg);
+            return { counts: { total: 0, successes: 0, failures: 0, alreadyExisted: 0 } };
           }

64-84: Quota check over-estimates usage by counting already-existing bookmarks.

The quota validation compares the raw count of parsed bookmarks (bookmarkCount at line 64) against remaining quota. However, the import pipeline later deduplicates bookmarks, and alreadyExisted items (line 175) don't consume additional quota. This means users near their quota limit are incorrectly blocked from importing files that contain mostly bookmarks they already have.

For example, a user with 1 slot remaining would be blocked from importing a file with 5 bookmarks if 4 of them already exist, even though the import would only add 1 new bookmark.

Consider one of these approaches:

Option 1: Move quota check after deduplication (recommended)

This requires restructuring to identify duplicates before creating bookmarks. The import logic would need to:

  1. Parse bookmarks
  2. Query existing bookmarks by URL/content hash
  3. Calculate actual new bookmarks
  4. Check quota against new count
  5. Proceed with import

Option 2: Document this as a known limitation

If fixing this is too complex for the current PR, clearly document that the quota check is conservative and may block imports that would succeed.

apps/web/components/settings/ImportExport.tsx (1)

140-146: Add a dismiss button to allow users to close the alert.

The alert appears when quotaError is set but provides no way for users to dismiss it. This creates a poor UX as the error message persists on screen even after the user has read it.

Apply this diff to add a dismiss button:

-      {quotaError && (
-        <Alert variant="destructive" className="relative">
-          <AlertCircle className="h-4 w-4" />
-          <AlertTitle>Import Quota Exceeded</AlertTitle>
-          <AlertDescription>{quotaError}</AlertDescription>
-        </Alert>
-      )}
+      {quotaError && (
+        <Alert variant="destructive" className="relative">
+          <AlertCircle className="h-4 w-4" />
+          <AlertTitle>{t("settings.import.quota_exceeded_title")}</AlertTitle>
+          <AlertDescription>{quotaError}</AlertDescription>
+          <button
+            onClick={clearQuotaError}
+            className="absolute right-2 top-2 rounded-md p-1 hover:bg-destructive/20"
+            aria-label="Dismiss"
+          >
+            <X className="h-4 w-4" />
+          </button>
+        </Alert>
+      )}

Note: This also addresses the i18n issue flagged in a previous review by using t() for the title. You'll need to add the translation key settings.import.quota_exceeded_title and import X from lucide-react.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 06a5676 and 833b2e9.

📒 Files selected for processing (2)
  • apps/web/components/settings/ImportExport.tsx (3 hunks)
  • apps/web/lib/hooks/useBookmarkImport.ts (5 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use TypeScript for type safety in all source files

Files:

  • apps/web/components/settings/ImportExport.tsx
  • apps/web/lib/hooks/useBookmarkImport.ts
**/*.{ts,tsx,js,jsx,json,css,md}

📄 CodeRabbit inference engine (AGENTS.md)

Format code using Prettier according to project standards

Files:

  • apps/web/components/settings/ImportExport.tsx
  • apps/web/lib/hooks/useBookmarkImport.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Lint code using oxlint and fix issues with pnpm lint:fix

Files:

  • apps/web/components/settings/ImportExport.tsx
  • apps/web/lib/hooks/useBookmarkImport.ts
apps/web/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/**/*.{ts,tsx}: Use Tailwind CSS for styling in the web application
Use Next.js for building the main web application

Files:

  • apps/web/components/settings/ImportExport.tsx
  • apps/web/lib/hooks/useBookmarkImport.ts
🧬 Code graph analysis (1)
apps/web/lib/hooks/useBookmarkImport.ts (2)
packages/shared/import-export/index.ts (1)
  • parseImportFile (4-4)
packages/shared/import-export/parsers.ts (1)
  • parseImportFile (336-365)
⏰ 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: tests
  • GitHub Check: typecheck
🔇 Additional comments (5)
apps/web/components/settings/ImportExport.tsx (1)

4-4: LGTM!

The new Alert-related imports are correctly used in the quota error UI.

Also applies to: 19-19

apps/web/lib/hooks/useBookmarkImport.ts (4)

16-16: LGTM!

The new imports and utilities are correctly set up to support quota checking before import.

Also applies to: 21-21, 43-43


41-41: LGTM!

The quotaError state is properly initialized and integrated into the hook's flow.


159-164: LGTM!

The custom parser optimization correctly reuses the already-parsed bookmarks, avoiding redundant parsing.


200-201: LGTM!

The hook correctly exposes quotaError and clearQuotaError to enable quota error handling in the UI.

Comment on lines +135 to +136
const { importProgress, quotaError, runUploadBookmarkFile } =
useBookmarkImport();
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 | 🟠 Major

Destructure clearQuotaError to enable dismissing the alert.

The hook provides clearQuotaError (see line 201 in useBookmarkImport.ts), but it's not destructured here. This means users cannot dismiss the quota error alert once it appears, forcing them to refresh the page or attempt another import.

Apply this diff to add dismiss functionality:

-  const { importProgress, quotaError, runUploadBookmarkFile } =
-    useBookmarkImport();
+  const { importProgress, quotaError, clearQuotaError, runUploadBookmarkFile } =
+    useBookmarkImport();
📝 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 { importProgress, quotaError, runUploadBookmarkFile } =
useBookmarkImport();
const { importProgress, quotaError, clearQuotaError, runUploadBookmarkFile } =
useBookmarkImport();
🤖 Prompt for AI Agents
In apps/web/components/settings/ImportExport.tsx around lines 135-136, the
useBookmarkImport hook return value is missing the clearQuotaError function, so
the quota error alert cannot be dismissed; update the destructuring to include
clearQuotaError from useBookmarkImport and wire that function to the alert
dismissal handler (e.g., onClose or dismiss action) so users can clear the quota
error without refreshing the page.

@MohamedBassem MohamedBassem merged commit 20d3761 into main Dec 8, 2025
11 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.

3 participants