-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Fix a bunch of folders bugs #2764
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughReplaces URL query-based folderId access with a new hook Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant Component as Component (e.g., Link UI)
participant Hook as useCurrentFolderId
participant NextNav as useSearchParams
participant Workspace as useWorkspace
User->>Component: Render
Component->>Hook: request { folderId }
Hook->>NextNav: read ?folderId
alt folderId present in URL
Hook-->>Component: { folderId }
else no folderId in URL
Hook->>Workspace: get defaultFolderId
alt defaultFolderId present
Hook-->>Component: { folderId: defaultFolderId }
else none or "unsorted"
Hook-->>Component: { folderId: null }
end
end
Component-->>User: Render UI using current folder context
sequenceDiagram
autonumber
participant UI as FolderDropdown
participant Hook as useCurrentFolderId
participant Data as useLinks / Prefetch
participant Router as Router/URL
UI->>Hook: Read currentFolderId
Note over UI,Hook: folderId = selectedFolderId || currentFolderId
UI->>Data: Prefetch links for folderId (including "unsorted")
User->>UI: Select "Unsorted" or a Folder
alt Select "Unsorted" and no defaultFolderId
UI->>Router: delete ?folderId
else Select folder or Unsorted with defaultFolderId
UI->>Router: set ?folderId=<chosen or unsorted>
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
Poem
✨ Finishing Touches
🧪 Generate unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
|
@coderabbitai full review |
✅ Actions performedFull review triggered. |
There was a problem hiding this 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
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (7)
apps/web/ui/modals/import-rebrandly-modal.tsx (1)
172-181: Add React keys to mapped domain rowsEach item in
domains.map(...)lacks akey, which can cause unstable renders and warnings.- {domains.map(({ id, domain, links }) => ( - <div className="flex items-center justify-between space-x-2 rounded-md border border-neutral-200 bg-white px-4 py-2"> + {domains.map(({ id, domain, links }) => ( + <div + key={id} + className="flex items-center justify-between space-x-2 rounded-md border border-neutral-200 bg-white px-4 py-2" + >apps/web/app/app.dub.co/(dashboard)/[slug]/links/page-client.tsx (1)
225-235: Avoid non-null assertion on possibly null folderIdWith the new hook,
folderIdcan benull(unsorted). PassingfolderId!risks runtime issues inRequestFolderEditAccessButton.- ) : ( - <div className="w-fit"> - <RequestFolderEditAccessButton - folderId={folderId!} - workspaceId={workspace.id!} - variant="primary" - /> - </div> - )} + ) : folderId ? ( + <div className="w-fit"> + <RequestFolderEditAccessButton + folderId={folderId} + workspaceId={workspace.id!} + variant="primary" + /> + </div> + ) : null}apps/web/ui/modals/import-short-modal.tsx (1)
160-191: Add React keys to mapped domain rowsSame issue as the Rebrandly modal: missing
keyon mapped elements.- {domains.map(({ id, domain, links }) => ( - <div className="flex items-center justify-between space-x-2 rounded-md border border-neutral-200 bg-white px-4 py-2"> + {domains.map(({ id, domain, links }) => ( + <div + key={id} + className="flex items-center justify-between space-x-2 rounded-md border border-neutral-200 bg-white px-4 py-2" + >apps/web/ui/modals/import-bitly-modal.tsx (1)
78-83: OAuth state isn’t URL-encoded — this can break or weaken the OAuth flow.state is a JSON string placed directly in the query string. Characters like quotes and braces must be encoded; otherwise the provider may reject the URL or misparse state. Encode the JSON payload.
Apply this diff:
- const bitlyOAuthURL = `https://bitly.com/oauth/authorize?client_id=${process.env.NEXT_PUBLIC_BITLY_CLIENT_ID}&redirect_uri=${APP_DOMAIN_WITH_NGROK}/api/callback/bitly&state=${JSON.stringify( + const bitlyOAuthURL = `https://bitly.com/oauth/authorize?client_id=${process.env.NEXT_PUBLIC_BITLY_CLIENT_ID}&redirect_uri=${APP_DOMAIN_WITH_NGROK}/api/callback/bitly&state=${encodeURIComponent(JSON.stringify( { workspaceId, ...(folderId ? { folderId } : {}), - }, - )}`; + }, + ))}`;Optional: build the URL with URL/URLSearchParams to avoid future encoding bugs.
const u = new URL("https://bitly.com/oauth/authorize"); u.searchParams.set("client_id", process.env.NEXT_PUBLIC_BITLY_CLIENT_ID!); u.searchParams.set("redirect_uri", `${APP_DOMAIN_WITH_NGROK}/api/callback/bitly`); u.searchParams.set("state", JSON.stringify({ workspaceId, ...(folderId ? { folderId } : {}) })); const bitlyOAuthURL = u.toString();apps/web/ui/links/use-link-filters.tsx (1)
36-104: Fix stale memo: include tagsAsync in dependencies.The
filtersmemo readstagsAsync(forshouldFilter) but it isn't in the dependency array, so toggling async mode won’t update the UI until another dep changes.Apply this diff:
- }, [domains, tags, users]); + }, [domains, tags, users, tagsAsync]);apps/web/ui/folders/folder-dropdown.tsx (2)
86-89: Avoid fetching “unsorted” via useFolder.
folderIdcan be"unsorted"(fromselectedFolderId). CallinguseFolderwith that will 404 or waste network. Disable the query for the sentinel.- const { folder: selectedFolderData } = useFolder({ - folderId, - enabled: !!folderId, - }); + const { folder: selectedFolderData } = useFolder({ + folderId, + enabled: !!(folderId && folderId !== "unsorted"), + });
117-130: Move prefetch side-effect out of useMemo.Side-effects inside
useMemois an antipattern and may be skipped. Prefetch in an effect; also prefetch the base path for “unsorted”.Apply this diff to remove the side-effect:
- if (folderId) { - router.prefetch(`/${slug}/links?folderId=${folderId}`); - }Then add this effect nearby (e.g., after line 114):
// Prefetch the next view for snappy nav useEffect(() => { if (!folderId) return; if (folderId === "unsorted") { router.prefetch(`/${slug}/links`); } else { router.prefetch(`/${slug}/links?folderId=${folderId}`); } }, [folderId, slug, router]);
🧹 Nitpick comments (13)
apps/web/ui/modals/set-default-folder-modal.tsx (1)
87-90: Disable action if workspaceId isn’t readyButton can be clicked when
workspaceIdis not loaded (early-return inside handler silently no-ops). Disable it until ready for clearer UX.- loading={isPending} + loading={isPending} + disabled={!workspaceId}apps/web/ui/modals/import-rebrandly-modal.tsx (1)
152-154: Optional: build the redirect URL via query helpers to preserve existing paramsUsing string templates drops any other existing query params. Consider
useRouterStuff().queryParams({ set: { folderId } })and thenrouter.pushor call it directly to maintain state.apps/web/lib/swr/use-current-folder-id.ts (1)
4-14: Add explicit return type for clarity and downstream typingHelps call sites reason about nullability and avoids implicit
anyregressions if this grows.-export default function useCurrentFolderId() { +export default function useCurrentFolderId(): { folderId: string | null } {apps/web/ui/modals/import-short-modal.tsx (1)
141-142: Optional: preserve existing query params when redirecting after importConsider using
useRouterStuff().queryParamsto setfolderIdrather than manually constructing?folderId=..., which drops other stateful params.apps/web/ui/modals/import-csv-modal/index.tsx (1)
220-222: URL-encode folderId when composing the redirect.If folderId ever contains non-URL-safe characters, the query string can break. Safer to encode the value.
Apply this diff:
- router.push( - `/${slug}/links${folderId ? `?folderId=${folderId}` : ""}`, - ); + router.push( + `/${slug}/links${ + folderId ? `?folderId=${encodeURIComponent(folderId)}` : "" + }`, + );apps/web/ui/links/links-container.tsx (2)
91-99: Empty-state “filtered” logic may ignore default-folder scoping.isFiltered only checks URL params. With the new hook, users can be scoped to a default folder even without a folderId param, which would still show the “No links yet” state (with CTA) instead of “No links found” (no CTA). If the intent is to treat default-folder scoping as a filter, thread that signal into LinksList.
Apply these diffs:
@@ return ( <PageWidthWrapper className="grid gap-y-2"> <LinksList CreateLinkButton={CreateLinkButton} links={links} count={count} loading={isValidating} compact={viewMode === "rows"} + folderScoped={!!folderId} /> </PageWidthWrapper> );@@ -function LinksList({ +function LinksList({ CreateLinkButton, links, count, loading, compact, + folderScoped, }: { CreateLinkButton: () => JSX.Element; links?: ResponseLink[]; count?: number; loading?: boolean; compact: boolean; + folderScoped: boolean; }) {@@ - const isFiltered = [ - "folderId", - "tagIds", - "domain", - "userId", - "search", - "showArchived", - ].some((param) => searchParams.has(param)); + const isFiltered = + folderScoped || + ["tagIds", "domain", "userId", "search", "showArchived"].some((param) => + searchParams.has(param), + );Also applies to: 54-61, 73-85
42-43: Replace empty‐string sentinel with undefined for folderId in useLinks callsTo ensure downstream query/serialization treats “unspecified” correctly, update both calls in apps/web/ui/links/links-container.tsx:
• At lines 42–43:
- folderId: folderId ?? "", + folderId: folderId ?? undefined,• At lines 48–49 (useLinksCount):
- folderId: folderId ?? "", + folderId: folderId ?? undefined,This preserves the existing typing (opts is a partial Zod‐inferred schema) while omitting folderId entirely when falsy, avoiding accidental
?folderId=emissions.apps/web/ui/modals/import-bitly-modal.tsx (1)
13-13: Sanity-check env configuration at build time.If NEXT_PUBLIC_BITLY_CLIENT_ID is missing, the generated OAuth URL will be invalid (client_id=undefined). Consider a build-time guard to surface misconfig early.
I can provide a tiny compile-time assertion or runtime console.warn gated to dev if useful.
apps/web/ui/links/link-controls.tsx (1)
103-105: Prefer nullish coalescing for folder selection.Use ?? rather than || to avoid treating empty strings as falsy. While folderId shouldn’t be "", using the nullish operator communicates intent and avoids accidental fallthrough.
Apply this diff:
- const { folderId: currentFolderId } = useCurrentFolderId(); - const folderId = link.folderId || currentFolderId; + const { folderId: currentFolderId } = useCurrentFolderId(); + const folderId = link.folderId ?? currentFolderId;apps/web/lib/swr/use-is-mega-folder.ts (1)
11-12: Minor boolean simplification.The ternary can be simplified for readability.
Apply this diff:
- isMegaFolder: folderId && folder?.type === "mega" ? true : false, + isMegaFolder: !!folderId && folder?.type === "mega",apps/web/ui/links/use-link-filters.tsx (2)
86-101: Remove the ts-expect-error by tightening return types from useUserFilterOptions.The
// @ts-expect-errormasks type drift between the two branches ofuseUserFilterOptions. Normalize the return type so this map is type-safe.Apply these changes:
- // @ts-expect-error - users?.map(({ id, name, email, image, count }) => ({ + users?.map(({ id, name, email, image, count }) => ({ value: id, label: name || email, icon: ( <Avatar user={{ id, name, image, }} className="h-4 w-4" /> ), right: nFormatter(count, { full: true }), })) ?? null,And update the helper’s signature and shape to be consistent (lower in the file):
-function useUserFilterOptions({ folderId }: { folderId: string }) { +type UserFilterOption = { + id: string; + name?: string; + email?: string; + image?: string; + count: number; +}; + +function useUserFilterOptions({ folderId }: { folderId: string }): UserFilterOption[] | null {
120-136: Guard against duplicate tagIds when selecting.Repeatedly selecting the same tag can introduce duplicates in
tagIds. Deduplicate when composing the param.- queryParams({ - set: { - tagIds: selectedTagIds.concat(value).join(","), - }, - del: "page", - }); + queryParams({ + set: { + tagIds: Array.from(new Set(selectedTagIds.concat(value))).join(","), + }, + del: "page", + });apps/web/ui/links/link-card.tsx (1)
80-83: Add editUrl to the prefetch effect deps.
router.prefetch(editUrl)depends oneditUrl. Add it to avoid linter noise and ensure correctness if props change.-useEffect(() => { - if (isInView) router.prefetch(editUrl); -}, [isInView]); +useEffect(() => { + if (isInView) router.prefetch(editUrl); +}, [isInView, editUrl]);
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (15)
apps/web/app/app.dub.co/(dashboard)/[slug]/links/page-client.tsx(2 hunks)apps/web/lib/swr/use-current-folder-id.ts(1 hunks)apps/web/lib/swr/use-folder-link-count.ts(0 hunks)apps/web/lib/swr/use-is-mega-folder.ts(1 hunks)apps/web/ui/folders/folder-dropdown.tsx(4 hunks)apps/web/ui/links/link-card.tsx(3 hunks)apps/web/ui/links/link-controls.tsx(3 hunks)apps/web/ui/links/link-title-column.tsx(4 hunks)apps/web/ui/links/links-container.tsx(2 hunks)apps/web/ui/links/use-link-filters.tsx(2 hunks)apps/web/ui/modals/import-bitly-modal.tsx(2 hunks)apps/web/ui/modals/import-csv-modal/index.tsx(2 hunks)apps/web/ui/modals/import-rebrandly-modal.tsx(2 hunks)apps/web/ui/modals/import-short-modal.tsx(2 hunks)apps/web/ui/modals/set-default-folder-modal.tsx(1 hunks)
💤 Files with no reviewable changes (1)
- apps/web/lib/swr/use-folder-link-count.ts
🧰 Additional context used
🧬 Code graph analysis (14)
apps/web/lib/swr/use-is-mega-folder.ts (1)
apps/web/lib/swr/use-current-folder-id.ts (1)
useCurrentFolderId(4-14)
apps/web/ui/links/link-controls.tsx (1)
apps/web/lib/swr/use-current-folder-id.ts (1)
useCurrentFolderId(4-14)
apps/web/app/app.dub.co/(dashboard)/[slug]/links/page-client.tsx (1)
apps/web/lib/swr/use-current-folder-id.ts (1)
useCurrentFolderId(4-14)
apps/web/ui/modals/import-short-modal.tsx (1)
apps/web/lib/swr/use-current-folder-id.ts (1)
useCurrentFolderId(4-14)
apps/web/ui/modals/import-bitly-modal.tsx (1)
apps/web/lib/swr/use-current-folder-id.ts (1)
useCurrentFolderId(4-14)
apps/web/ui/links/use-link-filters.tsx (1)
apps/web/lib/swr/use-current-folder-id.ts (1)
useCurrentFolderId(4-14)
apps/web/ui/modals/import-rebrandly-modal.tsx (1)
apps/web/lib/swr/use-current-folder-id.ts (1)
useCurrentFolderId(4-14)
apps/web/lib/swr/use-current-folder-id.ts (1)
apps/web/lib/swr/use-workspace.ts (1)
useWorkspace(6-45)
apps/web/ui/links/link-card.tsx (1)
apps/web/lib/swr/use-current-folder-id.ts (1)
useCurrentFolderId(4-14)
apps/web/ui/modals/set-default-folder-modal.tsx (1)
apps/web/lib/swr/use-workspace.ts (1)
useWorkspace(6-45)
apps/web/ui/modals/import-csv-modal/index.tsx (1)
apps/web/lib/swr/use-current-folder-id.ts (1)
useCurrentFolderId(4-14)
apps/web/ui/folders/folder-dropdown.tsx (1)
apps/web/lib/swr/use-current-folder-id.ts (1)
useCurrentFolderId(4-14)
apps/web/ui/links/links-container.tsx (1)
apps/web/lib/swr/use-current-folder-id.ts (1)
useCurrentFolderId(4-14)
apps/web/ui/links/link-title-column.tsx (2)
apps/web/lib/swr/use-workspace.ts (1)
useWorkspace(6-45)apps/web/lib/swr/use-current-folder-id.ts (1)
useCurrentFolderId(4-14)
⏰ 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). (1)
- GitHub Check: build
🔇 Additional comments (16)
apps/web/ui/modals/set-default-folder-modal.tsx (2)
26-26: Good move to use slug alongside workspaceIdDestructuring
slugfromuseWorkspace()aligns this modal with the slug-based SWR key used byuseWorkspace.
33-35: Folder SWR key usage is consistentAll folder-related SWR mutations and fetches in the codebase use the
workspaceIdquery parameter (e.g.mutate('/api/folders?workspaceId=…')), and there are no instances of slug-based folder keys (such as?workspaceSlug=or?slug=). Theset-default-folder-modal.tsxcall tomutate('/api/folders?workspaceId=${workspaceId}')matches the existing convention, so you can disregard the fragmentation concern raised earlier.Likely an incorrect or invalid review comment.
apps/web/ui/modals/import-rebrandly-modal.tsx (1)
1-1: Nice adoption of useCurrentFolderIdCentralizing folderId sourcing through the new hook removes URL-coupling and keeps behavior consistent with defaults.
apps/web/app/app.dub.co/(dashboard)/[slug]/links/page-client.tsx (2)
3-3: Good change: useCurrentFolderId importUsing the hook removes direct coupling to URL parsing across the page.
89-97: useCheckFolderPermission already handles null/“unsorted” folderIdThe implementation in apps/web/lib/swr/use-folder-permissions.ts returns true if folderId is falsy (e.g. null) or equals
"unsorted", so unsorted folders are granted access by design. No changes needed.apps/web/lib/swr/use-current-folder-id.ts (1)
4-14: Solid, minimal hook to source folderIdReading from search params with fallback to
defaultFolderIdand normalizing “unsorted” tonullis clean and predictable.apps/web/ui/modals/import-short-modal.tsx (1)
1-1: Good use of useCurrentFolderIdConsistent with the rest of the PR, removing direct URL parsing here improves cohesion.
apps/web/ui/modals/import-csv-modal/index.tsx (1)
4-4: Good migration to the centralized folderId source.Adopting useCurrentFolderId keeps folder scoping consistent across the app and avoids URL/searchParam drift. Appending folderId to FormData only when present is also correct.
Also applies to: 103-103, 208-208
apps/web/ui/links/links-container.tsx (1)
3-3: Hook adoption looks solid.Replacing ad-hoc searchParam/default logic with useCurrentFolderId reduces edge cases and aligns with the rest of the PR.
Also applies to: 37-37
apps/web/ui/modals/import-bitly-modal.tsx (1)
1-1: Folder scoping wired correctly.Using the new hook for folderId and threading it through state and POST payload keeps Bitly imports aligned with the current folder context.
Also applies to: 36-36, 138-139
apps/web/ui/links/link-controls.tsx (1)
146-149: I’ve requested the full implementation ofuseCheckFolderPermissionfor inspection. Once we see how it treatsfolderId === null, we can confirm whether an explicit workspace-level fallback is needed.apps/web/ui/links/use-link-filters.tsx (1)
199-205: Confirm folderId sentinel semantics for “unsorted”.You pass
folderIdasstring, withnullnormalized to"". EnsureuseLinksCountinterprets""as “unsorted/null folder” consistently across endpoints. If not, consider sendingundefinedinstead and handling defaulting inside the hook.apps/web/ui/links/link-card.tsx (1)
55-66: Consistent “show folder” condition with new hook — LGTM.Switching to
useCurrentFolderId()and keeping the “not default nor selected” check preserves existing UX. Nice.apps/web/ui/links/link-title-column.tsx (1)
108-116: Navigation consistencyGood call switching to a slug-based folder link here. Once the identity check above is fixed, this stays consistent with the “Open folder” banner in LinkCard.
apps/web/ui/folders/folder-dropdown.tsx (2)
96-101: Confirm redirect intent after folder creation.Current logic only redirects to the new folder if you were already in a folder view. If the user creates a folder from “All/Unsorted”, it won’t navigate into the new folder. Is that intended?
If not, consider always navigating to the new folder when auto-redirect is enabled:
- router.push( - `/${slug}/links${folderId && folderId !== "unsorted" ? `?folderId=${folder.id}` : ""}`, - ); + router.push(`/${slug}/links?folderId=${folder.id}`);
195-205: URL handling for “unsorted” with default folderGood handling: delete
folderIdonly when selecting “unsorted” and there is nodefaultFolderId; otherwise setfolderId=unsorted. This keeps URL state explicit when needed.
| const showFolderIcon = useMemo(() => { | ||
| return Boolean( | ||
| !loading && | ||
| link.folderId && | ||
| ![defaultFolderId, selectedFolderId].includes(link.folderId), | ||
| currentFolderId && | ||
| currentFolderId.includes(link.folderId), | ||
| ); | ||
| }, [loading, link.folderId, defaultFolderId, selectedFolderId]); | ||
| }, [loading, link.folderId, currentFolderId]); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: using String.includes to compare folder IDs leads to false positives.
currentFolderId.includes(link.folderId) performs substring matching, not identity. IDs like abc and ab will match incorrectly, toggling the icon unpredictably.
Match the logic in LinkCard and check identity against both the current and default folder:
- const { folderId: currentFolderId } = useCurrentFolderId();
+ const { folderId: currentFolderId } = useCurrentFolderId();
+ const { slug, defaultFolderId } = useWorkspace();
@@
- const showFolderIcon = useMemo(() => {
- return Boolean(
- !loading &&
- link.folderId &&
- currentFolderId &&
- currentFolderId.includes(link.folderId),
- );
- }, [loading, link.folderId, currentFolderId]);
+ const showFolderIcon = useMemo(() => {
+ return Boolean(
+ !loading &&
+ link.folderId &&
+ ![defaultFolderId ?? null, currentFolderId ?? null].includes(link.folderId),
+ );
+ }, [loading, link.folderId, defaultFolderId, currentFolderId]);📝 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.
| const showFolderIcon = useMemo(() => { | |
| return Boolean( | |
| !loading && | |
| link.folderId && | |
| ![defaultFolderId, selectedFolderId].includes(link.folderId), | |
| currentFolderId && | |
| currentFolderId.includes(link.folderId), | |
| ); | |
| }, [loading, link.folderId, defaultFolderId, selectedFolderId]); | |
| }, [loading, link.folderId, currentFolderId]); | |
| const { folderId: currentFolderId } = useCurrentFolderId(); | |
| const { slug, defaultFolderId } = useWorkspace(); | |
| - const showFolderIcon = useMemo(() => { | |
| - return Boolean( | |
| - !loading && | |
| - link.folderId && | |
| - currentFolderId && | |
| - currentFolderId.includes(link.folderId), | |
| - ); | |
| const showFolderIcon = useMemo(() => { | |
| return Boolean( | |
| !loading && | |
| link.folderId && | |
| ![defaultFolderId ?? null, currentFolderId ?? null].includes(link.folderId), | |
| ); | |
| }, [loading, link.folderId, defaultFolderId, currentFolderId]); |
🤖 Prompt for AI Agents
In apps/web/ui/links/link-title-column.tsx around lines 89 to 97, the
showFolderIcon logic uses currentFolderId.includes(link.folderId) which does
substring matching and causes false positives; change it to check identity like
currentFolderId === link.folderId || defaultFolderId === link.folderId to mirror
LinkCard logic, ensure you guard against undefined (e.g., Boolean(!loading &&
link.folderId && (currentFolderId === link.folderId || defaultFolderId ===
link.folderId))), and update the useMemo dependency array to include
defaultFolderId as well as the existing values.
| const { executeAsync, isPending } = useAction(setDefaultFolderAction, { | ||
| onSuccess: async () => { | ||
| setShowDefaultFolderModal(false); | ||
| await Promise.all([ | ||
| mutate("/api/workspaces"), | ||
| mutate(`/api/workspaces/${workspaceId}`), | ||
| mutate(`/api/workspaces/${slug}`), | ||
| mutate(`/api/folders?workspaceId=${workspaceId}`), | ||
| ]); | ||
| }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Avoid mutating a potentially undefined SWR key; use the bound mutate from useWorkspace()
If slug isn’t available yet, mutate(\/api/workspaces/${slug}`)will attempt to mutate an invalid key. Prefer the boundmutatereturned byuseWorkspace()and guard whenslug` is falsy.
Apply:
- const { id: workspaceId, slug } = useWorkspace();
+ const { id: workspaceId, slug, mutate: mutateWorkspace } = useWorkspace();
...
await Promise.all([
mutate("/api/workspaces"),
- mutate(`/api/workspaces/${slug}`),
+ slug ? mutateWorkspace() : Promise.resolve(),
mutate(`/api/folders?workspaceId=${workspaceId}`),
]);📝 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.
| const { executeAsync, isPending } = useAction(setDefaultFolderAction, { | |
| onSuccess: async () => { | |
| setShowDefaultFolderModal(false); | |
| await Promise.all([ | |
| mutate("/api/workspaces"), | |
| mutate(`/api/workspaces/${workspaceId}`), | |
| mutate(`/api/workspaces/${slug}`), | |
| mutate(`/api/folders?workspaceId=${workspaceId}`), | |
| ]); | |
| }, | |
| // Before: guards against undefined `slug` and uses the workspace’s own mutate | |
| const { id: workspaceId, slug, mutate: mutateWorkspace } = useWorkspace(); | |
| const { executeAsync, isPending } = useAction(setDefaultFolderAction, { | |
| onSuccess: async () => { | |
| setShowDefaultFolderModal(false); | |
| await Promise.all([ | |
| mutate("/api/workspaces"), | |
| // Only call the bound workspace-mutate if slug is defined | |
| slug ? mutateWorkspace() : Promise.resolve(), | |
| mutate(`/api/folders?workspaceId=${workspaceId}`), | |
| ]); | |
| }, | |
| // …other callbacks/config… | |
| }); |
🤖 Prompt for AI Agents
In apps/web/ui/modals/set-default-folder-modal.tsx around lines 28 to 36, the
success handler calls mutate(`/api/workspaces/${slug}`) which can run with an
undefined slug; replace that global mutate call with the bound mutate from
useWorkspace() (the hook that provides workspace-scoped mutate) and only invoke
it when slug is truthy — keep the other global mutate calls for
"/api/workspaces" and `/api/folders?workspaceId=${workspaceId}` as-is, but guard
the workspace-specific mutate with a simple if (slug) or conditional so you
never call mutate with an invalid key.
There was a problem hiding this 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 (1)
apps/web/ui/links/link-title-column.tsx (1)
78-94: Show folder icon only when link’s folder differs from both the current and the default folder (match LinkCard).Current check only excludes the current folder. Links in the default folder will still show a folder icon when viewed outside that folder, which diverges from LinkCard and leads to inconsistent UI. Include the defaultFolderId in the exclusion and update deps.
Apply:
- const { slug } = useWorkspace(); + const { slug, defaultFolderId } = useWorkspace(); @@ - const showFolderIcon = useMemo(() => { - return Boolean( - !loading && link.folderId && currentFolderId !== link.folderId, - ); - }, [loading, link.folderId, currentFolderId]); + const showFolderIcon = useMemo(() => { + return Boolean( + !loading && + link.folderId && + ![defaultFolderId ?? null, currentFolderId ?? null].includes( + link.folderId, + ), + ); + }, [loading, link.folderId, defaultFolderId, currentFolderId]);Please confirm LinkCard uses the same identity-based exclusion for both current and default folders so the behavior stays consistent across views.
🧹 Nitpick comments (1)
apps/web/ui/links/link-title-column.tsx (1)
105-113: Verify slug availability for the folder link href.This builds
/${slug}/links?folderId=.... If this component can render in contexts without a workspace slug (e.g., admin mode), the href becomes malformed. If that can’t happen, we’re good; otherwise guard or compute a safe fallback.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (3)
apps/web/lib/swr/use-current-folder-id.ts(1 hunks)apps/web/ui/links/link-card.tsx(3 hunks)apps/web/ui/links/link-title-column.tsx(4 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- apps/web/lib/swr/use-current-folder-id.ts
- apps/web/ui/links/link-card.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
apps/web/ui/links/link-title-column.tsx (2)
apps/web/lib/swr/use-workspace.ts (1)
useWorkspace(6-45)apps/web/lib/swr/use-current-folder-id.ts (1)
useCurrentFolderId(6-16)
🔇 Additional comments (2)
apps/web/ui/links/link-title-column.tsx (2)
3-3: LGTM: Switched to useCurrentFolderId.Good move away from ad-hoc URL parsing; centralizing folder resolution reduces edge cases (e.g., “unsorted” → null mapping).
45-45: LGTM: Added useParams import for UserAvatar.Required for the admin-mode conditional; import aligns with usage below.
Summary by CodeRabbit