Skip to content

Conversation

@visz11
Copy link

@visz11 visz11 commented Dec 15, 2025

CodeAnt-AI Description

Remove legacy link behavior and fix link/button prop handling across UI

What Changed

  • Next.js Link usage removed from places that previously wrapped native anchors; components now render plain links with className and props (e.g., stepper, list items, login/reschedule links, "done" and app-store buttons) so links behave and style consistently without legacy wrappers
  • Button now renders either a Link-style anchor (when href is present) or a native button; Link rendering no longer receives button-only props (disabled, type, ref) and data-testid is preserved, preventing incorrect attributes on anchors
  • Dropdown and other components strip ref when rendering a Link and render native buttons otherwise, avoiding incompatible ref/type propagation and preventing TypeScript errors and build failures
  • Small visual/markup fixes: allowed plain img usage for Make logo and adjusted spacing classes

Impact

✅ Fewer TypeScript build failures during API V1/V2 builds
✅ Links render with correct attributes and styling (no invalid disabled/type on anchors)
✅ More reliable navigation and button behavior across setup, auth, bookings, and list UIs

💡 Usage Guide

Checking Your Pull Request

Every time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later.

Talking to CodeAnt AI

Got a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask:

@codeant-ai ask: Your question here

This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.

Example

@codeant-ai ask: Can you suggest a safer alternative to storing this secret?

Preserve Org Learnings with CodeAnt

You can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input:

@codeant-ai: Your feedback here

This helps CodeAnt AI learn and adapt to your team's coding style and standards.

Example

@codeant-ai: Do not flag unused imports.

Retrigger review

Ask CodeAnt AI to review the PR again, by typing:

@codeant-ai: review

Check Your Repository Health

To analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health.

volnei and others added 10 commits December 3, 2025 12:55
…ut legacyBehavior

- Button.tsx: Only pass ref to button element, not to Link (Link manages its own anchor)
- Dropdown.tsx: Strip ref from props when using Link to avoid type incompatibility

This fixes the type errors that were causing API V1 and V2 builds to fail.

Co-Authored-By: Volnei Munhoz <[email protected]>
Link component doesn't accept disabled or type props, so these should only be passed when rendering a button element.

Co-Authored-By: Volnei Munhoz <[email protected]>
The passThroughProps spread was including a ref property that's incompatible with Link's expected ref type. This strips the ref when rendering a Link component.

Co-Authored-By: Volnei Munhoz <[email protected]>
… types

The Button component uses a union type for props that can be either Link or button props. TypeScript can't narrow the union type properly when using React.createElement with a dynamic element type, so we use a type assertion to cast the props to the correct type.

Co-Authored-By: Volnei Munhoz <[email protected]>
@codeant-ai
Copy link

codeant-ai bot commented Dec 15, 2025

CodeAnt AI is reviewing your PR.


Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@visz11
Copy link
Author

visz11 commented Dec 15, 2025

@refacto-visz

@coderabbitai
Copy link

coderabbitai bot commented Dec 15, 2025

Walkthrough

This pull request refactors Next.js Link usage patterns across multiple UI components and application modules. The changes remove legacy Link behaviors (legacyBehavior, passHref) and simplify navigation wrapper structures. Several components are updated to render Links and buttons through explicit conditional branches instead of dynamic element creation. The Button component now accepts href props directly for navigation. Link elements are repositioned from wrapper contexts to direct usage. Navigation destinations and overall component behaviors remain unchanged across all modifications.

Pre-merge checks and finishing touches

❌ 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 The pull request has no description provided by the author, making it impossible to verify alignment with the changeset. Add a pull request description explaining the motivation for removing legacyBehavior from Next.js Link components and the expected impact on the codebase.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title directly addresses the main change: removing Next.js Link legacyBehavior across multiple UI components throughout the codebase.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/remove-links-legacybehavior

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.

@gemini-code-assist
Copy link

Summary of Changes

Hello @visz11, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request focuses on modernizing the application's navigation by updating all instances of Next.js Link components to remove the deprecated legacyBehavior prop. It involves significant refactoring of several shared UI components to ensure they seamlessly integrate with the updated Link behavior, allowing for direct rendering of Link elements when navigation is intended. This change improves code maintainability, reduces boilerplate, and ensures compatibility with the latest Next.js routing paradigms.

Highlights

  • Next.js Link legacyBehavior Removal: The deprecated legacyBehavior prop has been removed from all Next.js Link components throughout the codebase, aligning with modern Next.js 13+ routing practices.
  • UI Component Refactoring: Core UI components like Button and ButtonOrLink have been refactored to natively handle Next.js Link components when an href prop is provided. This eliminates the need for Link wrappers with legacyBehavior.
  • Simplified Link Styling: Styling and event handlers are now directly applied to the Link component itself, rather than wrapping an <a> or <button> element inside Link, leading to a cleaner DOM structure and more straightforward component usage.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@codeant-ai codeant-ai bot added the size:L This PR changes 100-499 lines, ignoring generated files label Dec 15, 2025
@refacto-visz
Copy link

refacto-visz bot commented Dec 15, 2025


🔒 Limited Access – You're on the free plan.

🚀 Upgrade to Pro and unlock:

  • Unlimited AI PR reviews - No more restrictions
  • 🛡️ Code Quality Improvement with 50+ Standards (OWASP, CWE, Google-SRE)
  • 📊 Code Review Analytics - Track and improve your team's performance
  • 🎯 Specialized Reviews for Front-end, Backend, DevOps and other teams
  • 📈 Sequence diagrams and workflow charts
  • 📝 Enhanced summaries that help your team ship faster

👉 Upgrade to Pro Now

---### Refacto PR Summary
Removed deprecated Next.js Link legacyBehavior prop and migrated to modern Link API across the application. This modernization eliminates wrapper elements, simplifies Link usage, and aligns with Next.js 13+ best practices while maintaining identical functionality.

Key Changes:

  • Removed legacyBehavior and passHref props from Next.js Link components throughout the codebase
  • Migrated Button component to handle both Link and button rendering without type conflicts
  • Updated Dropdown and Stepper components to use direct Link styling instead of wrapper elements
  • Added ESLint disable comment for img element in Make setup component
  • Refactored authentication and booking components to use simplified Link syntax

Change Highlights

Click to expand
  • packages/ui/components/button/Button.tsx: Major refactor to handle Link vs button rendering separately
  • packages/ui/components/dropdown/Dropdown.tsx: Simplified ButtonOrLink component Link handling
  • packages/ui/components/form/step/Stepper.tsx: Direct Link styling instead of nested anchor elements
  • apps/web/components/apps/make/Setup.tsx: Removed Link wrapper, converted to Button href prop
  • apps/web/modules/bookings/views/bookings-single-view.tsx: Simplified reschedule/login Link components
  • packages/app-store/_components/AppNotInstalledMessage.tsx: Converted Link+Button to Button with href
  • packages/ui/components/list/List.tsx: Removed passHref and legacyBehavior props

Sequence Diagram

sequenceDiagram
    participant Dev as Developer
    participant App as Application
    participant Router as Next.js Router
    participant UI as UI Components
    
    Dev->>App: Click navigation element
    App->>UI: Render Link component
    UI->>UI: Check if href prop exists
    alt Has href (Link)
      UI->>Router: Navigate with Link
      Router-->>App: Route change
    else No href (Button)
      UI->>App: Execute onClick handler
      App-->>UI: Handle button action
    end
    App-->>Dev: Updated UI state
Loading

Testing Guide

Click to expand
  1. Navigation Testing: Click all navigation links throughout the app to ensure routing still works correctly
  2. Button Functionality: Test buttons with href props (like "Done" in Make setup) navigate properly
  3. Dropdown Links: Verify dropdown menu items with href props navigate without page refresh
  4. Stepper Navigation: Test step navigation in multi-step forms maintains proper routing
  5. Authentication Flows: Verify login/reschedule links in booking views work correctly

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

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

⚠️ Outside diff range comments (1)
packages/ui/components/list/List.tsx (1)

59-65: Critical: Nested anchors created when href is provided.

When href is provided:

  1. Line 40 sets elementType = "a"
  2. Lines 42-57 create an <a> element via createElement
  3. Line 60 wraps that anchor in <Link>

In Next.js 15, this produces nested anchors: <a><a>...</a></a>, which is invalid HTML and breaks navigation.

Apply this diff to fix:

 export function ListItem(props: ListItemProps) {
   const { href, expanded, rounded = true, ...passThroughProps } = props;
 
-  const elementType = href ? "a" : "li";
+  const elementType = href ? "div" : "li";
 
   const element = createElement(
     elementType,

By changing to "div", the Link will wrap a div instead of an anchor, preventing the nesting issue. The Link itself renders as an anchor in Next.js 15.

🧹 Nitpick comments (2)
packages/app-store/_components/AppNotInstalledMessage.tsx (1)

5-5: Consider using a named export instead of default export.

Named exports provide better tree-shaking, easier refactoring, and clearer imports.

As per coding guidelines.

Apply this diff:

-export default function AppNotInstalledMessage({ appName }: { appName: string }) {
+export function AppNotInstalledMessage({ appName }: { appName: string }) {
apps/web/components/apps/make/Setup.tsx (1)

73-73: Consider using Next.js Image component for better optimization.

While the ESLint disable allows the img tag, consider using next/image for automatic optimization, lazy loading, and proper sizing.

If the SVG endpoint supports it, replace with:

<Image 
  src="/api/app-store/make/icon.svg" 
  alt="Make Logo" 
  width={44} 
  height={44}
/>
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f350542 and 4c653c0.

📒 Files selected for processing (8)
  • apps/web/components/apps/make/Setup.tsx (2 hunks)
  • apps/web/modules/auth/forgot-password/[id]/forgot-password-single-view.tsx (1 hunks)
  • apps/web/modules/bookings/views/bookings-single-view.tsx (2 hunks)
  • packages/app-store/_components/AppNotInstalledMessage.tsx (1 hunks)
  • packages/ui/components/button/Button.tsx (2 hunks)
  • packages/ui/components/dropdown/Dropdown.tsx (1 hunks)
  • packages/ui/components/form/step/Stepper.tsx (1 hunks)
  • packages/ui/components/list/List.tsx (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx,js,jsx}

⚙️ CodeRabbit configuration file

Flag default exports and encourage named exports. Named exports provide better tree-shaking, easier refactoring, and clearer imports. Exempt main components like pages, layouts, and components that serve as the primary export of a module.

Files:

  • packages/ui/components/form/step/Stepper.tsx
  • packages/app-store/_components/AppNotInstalledMessage.tsx
  • packages/ui/components/dropdown/Dropdown.tsx
  • apps/web/modules/bookings/views/bookings-single-view.tsx
  • packages/ui/components/button/Button.tsx
  • apps/web/modules/auth/forgot-password/[id]/forgot-password-single-view.tsx
  • apps/web/components/apps/make/Setup.tsx
  • packages/ui/components/list/List.tsx
🧬 Code graph analysis (1)
packages/app-store/_components/AppNotInstalledMessage.tsx (1)
packages/ui/components/button/Button.tsx (1)
  • Button (221-361)
🔇 Additional comments (8)
apps/web/modules/auth/forgot-password/[id]/forgot-password-single-view.tsx (1)

48-52: LGTM! Link refactor aligns with Next.js 15.

The change from a wrapper pattern to direct Link usage with inline styling is correct and follows Next.js 15's simplified Link API.

packages/app-store/_components/AppNotInstalledMessage.tsx (1)

18-21: LGTM! Button with href correctly replaces Link wrapper.

The Button component properly handles href navigation internally (verified in Button.tsx), making the Link wrapper unnecessary.

packages/ui/components/form/step/Stepper.tsx (1)

29-61: LGTM! Stepper refactor correctly applies styles directly to Link.

The change from a single Link wrapper with conditional content to three separate Link branches is clean and maintains the same navigation behavior. Each Link properly wraps span elements (not anchors), avoiding nested anchor issues.

packages/ui/components/dropdown/Dropdown.tsx (1)

144-154: LGTM! Clean refactor with proper ref handling.

The explicit ref stripping when using Link is correct since Link manages its own anchor element. The added comment clearly documents this behavior.

apps/web/components/apps/make/Setup.tsx (1)

137-139: LGTM! Button with href correctly replaces Link wrapper.

Using Button's built-in href support simplifies the code while maintaining the same navigation behavior.

apps/web/modules/bookings/views/bookings-single-view.tsx (2)

809-816: LGTM! Simplified Link structure maintains functionality.

Removing the span wrapper and applying styles directly to Link is cleaner. The data-testid is correctly moved to the Link element.


839-848: LGTM! Consistent Link refactor.

Same clean pattern as the login link—removes wrapper and applies styles directly to Link while preserving the test identifier.

packages/ui/components/button/Button.tsx (1)

242-361: LGTM! Excellent refactor—clearer separation of Link vs button rendering.

The explicit branching for Link and button cases improves type safety and readability. Key improvements:

  • Reusable buttonContent eliminates duplication
  • Proper ref handling (Link manages its own anchor, button receives forwardedRef)
  • Explicit disabled state that includes loading
  • Clean type casts avoid prop conflicts

</Link>
</span>
<Link
href={`/auth/login?callbackUrl=${encodeURIComponent(
Copy link

Choose a reason for hiding this comment

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

Suggestion: The login link's callback URL always points to /booking/{uid} and ignores the current query string (e.g. ?cancel=true), so after logging in the user is redirected to the generic booking page instead of back to the exact state they were on, contradicting the "redirect to here" comment and breaking flows that rely on those query parameters. [logic error]

Severity Level: Minor ⚠️

Suggested change
href={`/auth/login?callbackUrl=${encodeURIComponent(
`${pathname}${searchParams?.toString() ? `?${searchParams.toString()}` : ""}`
Why it matters? ⭐

The PR currently encodes a callbackUrl that always points to /booking/${bookingInfo?.uid} and discards the current pathname/search params. The comment above the link explicitly says "redirect to here" — preserving the current query string (e.g. cancel/reschedule flags, rescheduledBy, seatReferenceUid) is necessary to restore the same UI state after login. The proposed change (encoding pathname + search params) fixes a real UX bug rather than a stylistic nit.

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** apps/web/modules/bookings/views/bookings-single-view.tsx
**Line:** 810:810
**Comment:**
	*Logic Error: The login link's callback URL always points to `/booking/{uid}` and ignores the current query string (e.g. `?cancel=true`), so after logging in the user is redirected to the generic booking page instead of back to the exact state they were on, contradicting the "redirect to here" comment and breaking flows that rely on those query parameters.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.

Comment on lines +813 to +816
className="underline"
data-testid="reschedule-link">
{t("login")}
</Link>
Copy link

Choose a reason for hiding this comment

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

Suggestion: The login link is marked with data-testid="reschedule-link", which semantically refers to the reschedule action; this conflates two different actions under the same test identifier and will cause automated tests or future code that target the reschedule control by test id to mistakenly interact with the login link instead. [possible bug]

Severity Level: Critical 🚨

Suggested change
className="underline"
data-testid="reschedule-link">
{t("login")}
</Link>
data-testid="login-link">
Why it matters? ⭐

The login Link introduced in the requiresLoginToUpdate block reuses data-testid="reschedule-link". That identifier is also used for the real reschedule control elsewhere in this component. Reusing the same test id for two different actions will cause brittle or incorrect automated tests (and makes it harder to target elements reliably). Renaming the login link's test id to something like "login-link" or "login-redirect" is a small, sensible change that prevents test collisions.

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** apps/web/modules/bookings/views/bookings-single-view.tsx
**Line:** 813:816
**Comment:**
	*Possible Bug: The login link is marked with `data-testid="reschedule-link"`, which semantically refers to the reschedule action; this conflates two different actions under the same test identifier and will cause automated tests or future code that target the reschedule control by test id to mistakenly interact with the login link instead.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.

Comment on lines +29 to +55
{index + 1 < props.step ? (
<Link
href={props.disableSteps ? "#" : `${href}?step=${index + 1}`}
shallow
replace
className="hover:bg-inverted block h-2.5 w-2.5 rounded-full bg-gray-600">
<span className="sr-only">{mapStep.title}</span>
</Link>
) : index + 1 === props.step ? (
<Link
href={props.disableSteps ? "#" : `${href}?step=${index + 1}`}
shallow
replace
className="relative flex items-center justify-center"
aria-current="step">
<span className="absolute flex h-5 w-5 p-px" aria-hidden="true">
<span className="bg-emphasis h-full w-full rounded-full" />
</span>
<span
className="relative block h-2.5 w-2.5 rounded-full bg-gray-600"
aria-hidden="true"
/>
<span className="sr-only">{mapStep.title}</span>
</Link>
) : (
<Link
href={props.disableSteps ? "#" : `${href}?step=${index + 1}`}
Copy link

Choose a reason for hiding this comment

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

Suggestion: When the flag to disable steps is set, the indicators are still rendered as clickable links pointing to "#", so users can still click them and cause hash navigation/page jumps instead of being truly disabled; rendering non-interactive elements when steps are disabled avoids this incorrect behavior. [logic error]

Severity Level: Minor ⚠️

Suggested change
{index + 1 < props.step ? (
<Link
href={props.disableSteps ? "#" : `${href}?step=${index + 1}`}
shallow
replace
className="hover:bg-inverted block h-2.5 w-2.5 rounded-full bg-gray-600">
<span className="sr-only">{mapStep.title}</span>
</Link>
) : index + 1 === props.step ? (
<Link
href={props.disableSteps ? "#" : `${href}?step=${index + 1}`}
shallow
replace
className="relative flex items-center justify-center"
aria-current="step">
<span className="absolute flex h-5 w-5 p-px" aria-hidden="true">
<span className="bg-emphasis h-full w-full rounded-full" />
</span>
<span
className="relative block h-2.5 w-2.5 rounded-full bg-gray-600"
aria-hidden="true"
/>
<span className="sr-only">{mapStep.title}</span>
</Link>
) : (
<Link
href={props.disableSteps ? "#" : `${href}?step=${index + 1}`}
{props.disableSteps ? (
index + 1 === props.step ? (
<span className="relative flex items-center justify-center" aria-current="step">
<span className="absolute flex h-5 w-5 p-px" aria-hidden="true">
<span className="bg-emphasis h-full w-full rounded-full" />
</span>
<span
className="relative block h-2.5 w-2.5 rounded-full bg-gray-600"
aria-hidden="true"
/>
<span className="sr-only">{mapStep.title}</span>
</span>
) : (
<span
className={
index + 1 < props.step
? "hover:bg-inverted block h-2.5 w-2.5 rounded-full bg-gray-600"
: "bg-emphasis block h-2.5 w-2.5 rounded-full hover:bg-gray-400"
}>
<span className="sr-only">{mapStep.title}</span>
</span>
)
) : index + 1 < props.step ? (
<Link
href={`${href}?step=${index + 1}`}
shallow
replace
className="hover:bg-inverted block h-2.5 w-2.5 rounded-full bg-gray-600">
<span className="sr-only">{mapStep.title}</span>
</Link>
) : index + 1 === props.step ? (
<Link
href={`${href}?step=${index + 1}`}
shallow
replace
className="relative flex items-center justify-center"
aria-current="step">
<span className="absolute flex h-5 w-5 p-px" aria-hidden="true">
<span className="bg-emphasis h-full w-full rounded-full" />
</span>
<span
className="relative block h-2.5 w-2.5 rounded-full bg-gray-600"
aria-hidden="true"
/>
<span className="sr-only">{mapStep.title}</span>
</Link>
) : (
<Link
href={`${href}?step=${index + 1}`}
Why it matters? ⭐

The PR currently conditionally sets href to "#" when props.disableSteps is true, but still renders an interactive Link. That allows users to click and change the URL hash (or cause focus/navigation), which is behaviour the flag intends to prevent. Replacing Link with non-interactive spans (or disabling pointer events / removing href) when disableSteps is true fixes a real UX bug and avoids accidental navigation. This is a relevant, non-cosmetic correctness fix scoped to this component.

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** packages/ui/components/form/step/Stepper.tsx
**Line:** 29:55
**Comment:**
	*Logic Error: When the flag to disable steps is set, the indicators are still rendered as clickable links pointing to "#", so users can still click them and cause hash navigation/page jumps instead of being truly disabled; rendering non-interactive elements when steps are disabled avoids this incorrect behavior.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.

@codeant-ai
Copy link

codeant-ai bot commented Dec 15, 2025

Nitpicks 🔍

🔒 No security issues identified
⚡ Recommended areas for review

  • Invalid anchor attributes
    The Link render path spreads passThroughProps (which may include button-only props such as disabled and type) into the Next.js Link. Those props are invalid on anchors and may produce incorrect DOM attributes, TypeScript mismatches and accessibility problems. The code currently only narrows out href, onClick, and ref in the cast, but not disabled/type.

  • Nested anchor / missing href
    The code creates an 'a' element via createElement when href is present, then wraps that element in Next.js <Link href={href}>. Because passHref/legacyBehavior were removed, the inner anchor may not receive an href (or you can end up with nested anchors), producing invalid HTML and broken keyboard navigation. This should be reconciled by either letting Link render the anchor or rendering a native anchor directly (and ensuring it has an href).

  • Accessibility: disabled links still tabbable
    When disabled is true and an anchor (Link) is rendered, the link remains focusable and can be read as interactive by assistive tech. There's no aria-disabled or tabIndex handling for the link variant, which can confuse users and fail accessibility expectations.

  • Type narrowing and casting risk
    The runtime check isLink is fine, but the code relies on a broad cast of passThroughProps to an anchor props type. This hides the presence of incompatible props and shifts responsibility to runtime. Consider explicitly separating anchor/button props before spreading them to avoid silent mistakes.

  • Invalid prop on anchor
    The added Button is rendered with both href and type="button". If Button renders an anchor when href is present, type is not a valid anchor attribute and may cause React/DOM warnings or unexpected behavior. Ensure Button strips button-only props when rendering anchors or remove type here.

@codeant-ai
Copy link

codeant-ai bot commented Dec 15, 2025

CodeAnt AI finished reviewing your PR.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request successfully removes legacyBehavior from next/link components across the codebase, which is a great step towards modernizing the Next.js usage. The changes in the Button, Dropdown, and List components are well-implemented to encapsulate link behavior. The other files are updated accordingly to use these refactored components. The code is cleaner and follows current Next.js best practices. I have one suggestion to reduce code duplication in the Stepper component for better maintainability.

Comment on lines +29 to +61
{index + 1 < props.step ? (
<Link
href={props.disableSteps ? "#" : `${href}?step=${index + 1}`}
shallow
replace
className="hover:bg-inverted block h-2.5 w-2.5 rounded-full bg-gray-600">
<span className="sr-only">{mapStep.title}</span>
</Link>
) : index + 1 === props.step ? (
<Link
href={props.disableSteps ? "#" : `${href}?step=${index + 1}`}
shallow
replace
className="relative flex items-center justify-center"
aria-current="step">
<span className="absolute flex h-5 w-5 p-px" aria-hidden="true">
<span className="bg-emphasis h-full w-full rounded-full" />
</span>
<span
className="relative block h-2.5 w-2.5 rounded-full bg-gray-600"
aria-hidden="true"
/>
<span className="sr-only">{mapStep.title}</span>
</Link>
) : (
<Link
href={props.disableSteps ? "#" : `${href}?step=${index + 1}`}
shallow
replace
className="bg-emphasis block h-2.5 w-2.5 rounded-full hover:bg-gray-400">
<span className="sr-only">{mapStep.title}</span>
</Link>
)}

Choose a reason for hiding this comment

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

medium

The logic for rendering the different states of a stepper item is duplicated across three separate <Link> components. This repetition makes the code harder to read and maintain. Consider refactoring to use a single <Link> component and conditionally determine its className and children based on the step's state. This will make the component more concise and easier to manage.

                {(() => {
                  const isCompleted = index + 1 < props.step;
                  const isCurrent = index + 1 === props.step;

                  return (
                    <Link
                      href={props.disableSteps ? "#" : `${href}?step=${index + 1}`}
                      shallow
                      replace
                      className={
                        isCompleted
                          ? "hover:bg-inverted block h-2.5 w-2.5 rounded-full bg-gray-600"
                          : isCurrent
                          ? "relative flex items-center justify-center"
                          : "bg-emphasis block h-2.5 w-2.5 rounded-full hover:bg-gray-400"
                      }
                      aria-current={isCurrent ? "step" : undefined}>
                      {isCurrent && (
                        <>
                          <span className="absolute flex h-5 w-5 p-px" aria-hidden="true">
                            <span className="bg-emphasis h-full w-full rounded-full" />
                          </span>
                          <span
                            className="relative block h-2.5 w-2.5 rounded-full bg-gray-600"
                            aria-hidden="true"
                          />
                        </>
                      )}
                      <span className="sr-only">{mapStep.title}</span>
                    </Link>
                  );
                })()}

@visz11
Copy link
Author

visz11 commented Dec 15, 2025

@refacto-visz

@refacto-visz
Copy link

refacto-visz bot commented Dec 15, 2025

Refacto is reviewing this PR. Please wait for the review comments to be posted.

@refacto-visz
Copy link

refacto-visz bot commented Dec 15, 2025

Code Review: Next.js Link Modernization

PR Confidence Score: 🟨 4 / 5

👍 Well Done
Modernized Link Component Usage

Successfully refactored Next.js Link components removing legacyBehavior and passHref, aligning with modern Next.js practices and improving type safety.

Improved Component Abstraction

Enhanced Button component to handle href props internally, simplifying usage patterns and reducing developer complexity.

📁 Selected files for review (8)
  • apps/web/components/apps/make/Setup.tsx
  • apps/web/modules/auth/forgot-password/[id]/forgot-password-single-view.tsx
  • apps/web/modules/bookings/views/bookings-single-view.tsx
  • packages/app-store/_components/AppNotInstalledMessage.tsx
  • packages/ui/components/button/Button.tsx
  • packages/ui/components/dropdown/Dropdown.tsx
  • packages/ui/components/form/step/Stepper.tsx
  • packages/ui/components/list/List.tsx
📝 Additional Comments
packages/ui/components/button/Button.tsx (2)
Incompatible Props on Link

The passThroughProps object may contain props valid for button but not for Link component, such as type. While TypeScript casting is used, this doesn't prevent invalid HTML attributes from being rendered on the anchor tag at runtime, potentially leading to unexpected behavior or invalid HTML.

Standards:

  • Logic-Verification-Data-Flow
Inconsistent Tooltip Behavior

The refactored Button component has inconsistent tooltip behavior where the tooltip prop only works for button elements, not for buttons rendered as links. The Wrapper component responsible for tooltips is not applied to the Link component path, creating inconsistent behavior and reducing component reusability.

Standards:

  • Maintainability-Quality-Cohesion
  • Clean-Code-Functions
apps/web/components/apps/make/Setup.tsx (2)
Missing Link Validation

Button component accepts href prop without URL validation. While internal route appears safe, implementing URL validation would prevent potential open redirect vulnerabilities if href becomes user-controllable in future modifications.

Standards:

  • CWE-601
  • OWASP-A03
XSS Prevention Enhancement

Direct img element usage bypasses Next.js Image component security features. While current static path appears safe, Next.js Image provides automatic XSS protection and content validation for enhanced security posture.

Standards:

  • CWE-79
  • OWASP-A03

Comment on lines +60 to 62
<Link href={href}>
{element}
</Link>
Copy link

Choose a reason for hiding this comment

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

Invalid Nested Link Tag

The refactor to remove legacyBehavior incorrectly wraps a manually created anchor tag (element) inside the Link component. This results in invalid HTML with nested anchor tags, which can cause unpredictable rendering, styling, accessibility, and routing behavior in browsers.

    <Link href={href} {...element.props}>
      {element.props.children}
    </Link>
Commitable Suggestion
Suggested change
<Link href={href}>
{element}
</Link>
<Link href={href} {...element.props}>
{element.props.children}
</Link>
Standards
  • ISO-IEC-25010-Functional-Correctness-Appropriateness

Comment on lines +331 to +341
if (isLink) {
return (
<Link
{...(passThroughProps as Omit<JSX.IntrinsicElements["a"], "href" | "onClick" | "ref"> & LinkProps)}
shallow={shallow && shallow}
className={buttonClassName}
onClick={handleClick}>
{buttonContent}
</Link>
);
}
Copy link

Choose a reason for hiding this comment

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

Broken Ref Forwarding

The component's forwardRef contract promises to forward refs to both button and anchor elements. However, the refactored code only forwards refs when rendering a button, not when rendering a Link component. This breaks the component's contract and will cause refs to be ignored on any Button with an href prop.

Standards
  • ISO-IEC-25010-Functional-Correctness-Appropriateness
  • DbC-Postcondition-Violation

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L This PR changes 100-499 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants