-
Notifications
You must be signed in to change notification settings - Fork 10.8k
fix(ui): migrate off Next.js <Link legacyBehavior> — fix hydration & ref-forwarding issues #24418
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
base: main
Are you sure you want to change the base?
Conversation
Hey there and thank you for opening this pull request! 👋🏼 We require pull request titles to follow the Conventional Commits specification and it looks like your proposed title needs to be adjusted. Details:
|
@livebitz1 is attempting to deploy a commit to the cal Team on Vercel. A member of the Team first needs to authorize it. |
WalkthroughThis change migrates Next.js Link usage off Possibly related PRs
Pre-merge checks and finishing touches❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
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 (3)
packages/app-store/_components/AppNotInstalledMessage.tsx (1)
19-26
: Nested interactive elements violate the PR's accessibility goals.The current implementation wraps a
Button
inside aLink
, creating nested interactive elements (an anchor containing a button). This pattern:
- Violates HTML validity (interactive elements shouldn't be nested)
- Creates accessibility issues for keyboard navigation and screen readers
- Contradicts the PR's stated objective to "prevent nested interactive elements (anchors/buttons inside each other)"
Per the PR summary,
Button
now supports anhref
prop that internally renders aLink
. Use that pattern instead.Apply this diff:
<div className="mt-5"> - <Link href={`/apps/${appName}`} className="inline-block"> - <Button type="button" color="secondary"> - {t("go_to_app_store")} - <Icon name="arrow-up-right" className="ml-1" size={20} /> - </Button> - </Link> + <Button href={`/apps/${appName}`} type="button" color="secondary"> + {t("go_to_app_store")} + <Icon name="arrow-up-right" className="ml-1" size={20} /> + </Button> </div>apps/web/modules/auth/forgot-password/[id]/forgot-password-single-view.tsx (1)
48-54
: Remove nested interactive elements (Link > button).The Link wrapping a button element creates nested interactive elements, which violates accessibility guidelines and can break keyboard navigation and screen reader behavior.
Apply this diff to fix the issue by using just the Link element with appropriate styling:
- <Link href="/auth/forgot-password" className="flex w-full justify-center"> - <button - type="button" - className="flex w-full justify-center px-4 py-2 text-sm font-medium text-blue-600 focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2"> - {t("try_again")} - </button> - </Link> + <Link + href="/auth/forgot-password" + className="flex w-full justify-center px-4 py-2 text-sm font-medium text-blue-600 focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2"> + {t("try_again")} + </Link>packages/ui/components/button/Button.tsx (1)
323-368
: Tooltip support broken for links.The
Wrapper
component (which provides tooltip functionality) only wraps the button element (lines 359-368) but not the Link element (lines 323-340). This means tooltips won't display whenhref
is provided.Apply this diff to wrap both Link and button in the Wrapper:
const linkClass = classNames(buttonClasses({ color, size, loading, variant }), props.className); + const element = props.href ? ( - if (props.href) { - return ( <Link data-testid="link-component" href={props.href} shallow={shallow} // Link forwards refs to the underlying anchor automatically with the new Link API ref={forwardedRef as React.Ref<HTMLAnchorElement>} className={linkClass} onClick={ disabled ? (e: React.MouseEvent) => e.preventDefault() : (props.onClick as unknown as React.MouseEventHandler<HTMLAnchorElement>) }> {content} </Link> + ) - ); - } - - const element = React.createElement( + ) : React.createElement( "button", { ...passThroughProps, disabled, type: !isLink ? type : undefined, ref: forwardedRef, className: linkClass, onClick: disabled ? (e: React.MouseEvent<HTMLElement, MouseEvent>) => { e.preventDefault(); } : props.onClick, }, content ); return ( <Wrapper data-testid="wrapper" tooltip={props.tooltip} tooltipSide={tooltipSide} tooltipOffset={tooltipOffset} tooltipClassName={tooltipClassName}> {element} </Wrapper> ); });
🧹 Nitpick comments (3)
packages/ui/components/button/Button.tsx (2)
335-335
: Avoid unsafe type casting throughunknown
.The onClick handler uses
as unknown as
casting which completely bypasses TypeScript's type safety. This pattern can hide type errors.Apply this diff to use a more type-safe approach:
onClick={ disabled ? (e: React.MouseEvent) => e.preventDefault() - : (props.onClick as unknown as React.MouseEventHandler<HTMLAnchorElement>) + : (props.onClick as React.MouseEventHandler<HTMLAnchorElement> | undefined) }>
347-347
: Remove redundant condition.The check
!isLink ? type : undefined
is redundant since this code path only executes when!props.href
(which means!isLink
).Apply this diff:
...passThroughProps, disabled, - type: !isLink ? type : undefined, + type: type, ref: forwardedRef,packages/ui/components/dropdown/Dropdown.tsx (1)
157-159
: No incompatible prop usage found; consider refactoringDropdownItem
to use a discriminated union forprops
in Dropdown.tsx (lines 157–159, 167–169) to enforce correct attribute sets and avoid unchecked spreads.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
⛔ Files ignored due to path filters (1)
yarn.lock
is excluded by!**/yarn.lock
,!**/*.lock
📒 Files selected for processing (13)
PR_DESCRIPTION.md
(1 hunks)apps/web/components/apps/make/Setup.tsx
(2 hunks)apps/web/components/apps/zapier/Setup.tsx
(3 hunks)apps/web/modules/auth/forgot-password/[id]/forgot-password-single-view.tsx
(1 hunks)apps/web/modules/bookings/views/bookings-single-view.tsx
(7 hunks)package.json
(1 hunks)packages/app-store/_components/AppNotInstalledMessage.tsx
(1 hunks)packages/features/shell/CalAiBanner.tsx
(2 hunks)packages/lib/turndownService.ts
(1 hunks)packages/ui/components/button/Button.tsx
(3 hunks)packages/ui/components/dropdown/Dropdown.tsx
(2 hunks)packages/ui/components/form/step/Stepper.tsx
(1 hunks)packages/ui/components/list/List.tsx
(4 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
**/*.tsx
📄 CodeRabbit inference engine (.cursor/rules/review.mdc)
Always use
t()
for text localization in frontend code; direct text embedding should trigger a warning
Files:
packages/ui/components/form/step/Stepper.tsx
apps/web/components/apps/make/Setup.tsx
apps/web/modules/bookings/views/bookings-single-view.tsx
packages/app-store/_components/AppNotInstalledMessage.tsx
packages/ui/components/list/List.tsx
packages/ui/components/button/Button.tsx
apps/web/modules/auth/forgot-password/[id]/forgot-password-single-view.tsx
packages/features/shell/CalAiBanner.tsx
apps/web/components/apps/zapier/Setup.tsx
packages/ui/components/dropdown/Dropdown.tsx
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/review.mdc)
Flag excessive Day.js use in performance-critical code; prefer native Date or Day.js
.utc()
in hot paths like loops
Files:
packages/ui/components/form/step/Stepper.tsx
apps/web/components/apps/make/Setup.tsx
apps/web/modules/bookings/views/bookings-single-view.tsx
packages/app-store/_components/AppNotInstalledMessage.tsx
packages/ui/components/list/List.tsx
packages/ui/components/button/Button.tsx
apps/web/modules/auth/forgot-password/[id]/forgot-password-single-view.tsx
packages/lib/turndownService.ts
packages/features/shell/CalAiBanner.tsx
apps/web/components/apps/zapier/Setup.tsx
packages/ui/components/dropdown/Dropdown.tsx
**/*.{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
apps/web/components/apps/make/Setup.tsx
apps/web/modules/bookings/views/bookings-single-view.tsx
packages/app-store/_components/AppNotInstalledMessage.tsx
packages/ui/components/list/List.tsx
packages/ui/components/button/Button.tsx
apps/web/modules/auth/forgot-password/[id]/forgot-password-single-view.tsx
packages/lib/turndownService.ts
packages/features/shell/CalAiBanner.tsx
apps/web/components/apps/zapier/Setup.tsx
packages/ui/components/dropdown/Dropdown.tsx
**/*Service.ts
📄 CodeRabbit inference engine (.cursor/rules/review.mdc)
Service files must include
Service
suffix, use PascalCase matching exported class, and avoid generic names (e.g.,MembershipService.ts
)
Files:
packages/lib/turndownService.ts
**/*.ts
📄 CodeRabbit inference engine (.cursor/rules/review.mdc)
**/*.ts
: For Prisma queries, only select data you need; never useinclude
, always useselect
Ensure thecredential.key
field is never returned from tRPC endpoints or APIs
Files:
packages/lib/turndownService.ts
🧬 Code graph analysis (3)
apps/web/components/apps/make/Setup.tsx (1)
packages/ui/components/button/Button.tsx (1)
Button
(221-369)
packages/features/shell/CalAiBanner.tsx (1)
packages/lib/webstorage.ts (1)
localStorage
(6-36)
apps/web/components/apps/zapier/Setup.tsx (1)
packages/ui/components/button/Button.tsx (1)
Button
(221-369)
⏰ Context from checks skipped due to timeout of 180000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Install dependencies / Yarn install & cache
- GitHub Check: Codacy Static Code Analysis
🔇 Additional comments (12)
packages/ui/components/form/step/Stepper.tsx (1)
29-42
: LGTM! Clean refactor of Stepper Link rendering.The simplified approach using a single Link element with dynamic className and proper aria-current attribute is cleaner and more maintainable than the previous multi-branch conditional rendering. The accessibility features are properly preserved.
packages/lib/turndownService.ts (1)
41-58
: LGTM! Robust null safety improvements.The refactored
isShiftEnter
function now properly handles null/undefined nodes, traverses to the nearest element node, and includes defensive checks for parent and childNodes. This addresses the runtime null parentNode errors mentioned in the PR objectives.PR_DESCRIPTION.md (1)
1-114
: LGTM! Comprehensive PR documentation.The PR description provides excellent context for the migration, including motivation, scope, manual QA steps, and a reviewer checklist. This documentation will help ensure thorough review and testing of the changes.
packages/features/shell/CalAiBanner.tsx (1)
14-24
: LGTM! Hydration-safe visibility management.The refactor defers localStorage checks to
useEffect
, starting with a hidden state (false
) to ensure deterministic SSR output. The effect properly guards against SSR context and sets visibility based on pathname and localStorage. This prevents hydration mismatch warnings.apps/web/modules/bookings/views/bookings-single-view.tsx (3)
800-807
: LGTM! Clean Link usage without nested elements.The login and reschedule links now use direct Link components with proper styling, removing unnecessary nested spans. This maintains accessibility and aligns with the new Link API pattern.
Also applies to: 830-839
1107-1110
: LGTM! RescheduledToLink refactored correctly.The component now renders a direct Link with an inline icon, removing the previous nested structure. The flex layout with gap provides clean spacing while maintaining accessibility.
381-381
: Verify removal of old variable referencesPlease confirm there are no remaining occurrences of
canCancelAndReschedule
,canCancel
, orcanReschedule
in apps/web/modules/bookings/views/bookings-single-view.tsx.packages/ui/components/list/List.tsx (2)
1-2
: LGTM! Proper "use client" directive.The "use client" directive is correctly placed at the top of the file, which is necessary for using React hooks like
useLocale
in Next.js app directory.
35-57
: LGTM! Clean separation of ListItem and ListLinkItem.The refactor removes
href
support fromListItem
(enforcinghref?: never
in types) and ensures it always renders as a plainli
element. This prevents nested interactive elements and provides clear separation: useListItem
for static list items,ListLinkItem
for navigable items.package.json (1)
142-143
: Pinned @radix-ui/[email protected] exists and has no known vulnerabilities.apps/web/components/apps/make/Setup.tsx (1)
136-138
: LGTM!The migration to
Button[href]
correctly leverages the updated Button component API, eliminating the need for Link wrappers and addressing legacy behavior issues.apps/web/components/apps/zapier/Setup.tsx (1)
13-13
: LGTM!The explicit comment documenting the migration strategy and the consistent use of
Button[href]
align with the PR objectives and Button component updates.Also applies to: 142-144
@@ -1,5 +1,6 @@ | |||
"use client"; | |||
|
|||
/* eslint-disable */ |
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 | 🟠 Major
Use targeted ESLint rule disables instead of blanket disable.
The blanket eslint-disable
comment disables all linting for the entire file, which can hide legitimate issues and makes the codebase harder to maintain.
Replace with targeted rule disables only where necessary, for example:
-/* eslint-disable */
+/* eslint-disable react-hooks/exhaustive-deps */
Or better yet, fix the specific linting issues and remove the need for disables altogether.
📝 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.
/* eslint-disable */ | |
/* eslint-disable react-hooks/exhaustive-deps */ |
🤖 Prompt for AI Agents
In apps/web/modules/bookings/views/bookings-single-view.tsx around line 3 the
file uses a blanket "/* eslint-disable */" which disables all linting; remove
that top-level disable, run ESLint to identify the specific violations, fix them
where possible, and if a disable is still required use targeted comments (either
inline "// eslint-disable-next-line <rule>" for a single line or "/*
eslint-disable <rule1>, <rule2> */" scoped narrowly) only around the offending
code so linting remains active for the rest of the file.
export const ButtonOrLink = forwardRef<HTMLAnchorElement | HTMLButtonElement, ButtonOrLinkProps>( | ||
({ href, children, ...props }, forwardedRef) => { | ||
const isLink = typeof href !== "undefined"; | ||
|
||
if (isLink) { | ||
// Forward ref and anchor props to Next's Link. Use explicit casts to the correct HTML attribute types | ||
// to avoid `any` and satisfy ESLint rules. | ||
return ( | ||
<Link | ||
href={href as string} | ||
ref={forwardedRef as Ref<HTMLAnchorElement>} | ||
{...(props as AnchorHTMLAttributes<HTMLAnchorElement>)}> | ||
{children} | ||
</Link> | ||
); | ||
} | ||
|
||
if (isLink) { | ||
return ( | ||
<Link href={href} legacyBehavior> | ||
{content} | ||
</Link> | ||
<button | ||
ref={forwardedRef as Ref<HTMLButtonElement>} | ||
{...(props as ButtonHTMLAttributes<HTMLButtonElement>)}> | ||
{children} | ||
</button> | ||
); | ||
} | ||
|
||
return content; | ||
} | ||
); | ||
ButtonOrLink.displayName = "ButtonOrLink"; |
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.
Type casts bypass safety; consider discriminated union.
Multiple explicit type casts (as string
, as Ref<...>
, as AnchorHTMLAttributes<...>
) bypass TypeScript's type checking. Additionally, the current intersection type (lines 142-146) allows invalid prop combinations (e.g., both type
and target
simultaneously).
Consider refactoring to use a discriminated union:
-type ButtonOrLinkProps = ButtonHTMLAttributes<HTMLButtonElement> &
- AnchorHTMLAttributes<HTMLAnchorElement> & {
- href?: string;
- children?: React.ReactNode;
- };
+type ButtonOrLinkProps =
+ | ({ href: string } & AnchorHTMLAttributes<HTMLAnchorElement> & { children?: React.ReactNode })
+ | ({ href?: never } & ButtonHTMLAttributes<HTMLButtonElement> & { children?: React.ReactNode });
export const ButtonOrLink = forwardRef<HTMLAnchorElement | HTMLButtonElement, ButtonOrLinkProps>(
({ href, children, ...props }, forwardedRef) => {
- const isLink = typeof href !== "undefined";
-
- if (isLink) {
- // Forward ref and anchor props to Next's Link. Use explicit casts to the correct HTML attribute types
- // to avoid `any` and satisfy ESLint rules.
+ if (href) {
return (
<Link
- href={href as string}
- ref={forwardedRef as Ref<HTMLAnchorElement>}
- {...(props as AnchorHTMLAttributes<HTMLAnchorElement>)}>
+ href={href}
+ ref={forwardedRef as Ref<HTMLAnchorElement>}
+ {...props}>
{children}
</Link>
);
}
return (
<button
ref={forwardedRef as Ref<HTMLButtonElement>}
- {...(props as ButtonHTMLAttributes<HTMLButtonElement>)}>
+ {...props}>
{children}
</button>
);
}
);
This approach provides compile-time guarantees that appropriate props are passed to each element type.
📝 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.
export const ButtonOrLink = forwardRef<HTMLAnchorElement | HTMLButtonElement, ButtonOrLinkProps>( | |
({ href, children, ...props }, forwardedRef) => { | |
const isLink = typeof href !== "undefined"; | |
if (isLink) { | |
// Forward ref and anchor props to Next's Link. Use explicit casts to the correct HTML attribute types | |
// to avoid `any` and satisfy ESLint rules. | |
return ( | |
<Link | |
href={href as string} | |
ref={forwardedRef as Ref<HTMLAnchorElement>} | |
{...(props as AnchorHTMLAttributes<HTMLAnchorElement>)}> | |
{children} | |
</Link> | |
); | |
} | |
if (isLink) { | |
return ( | |
<Link href={href} legacyBehavior> | |
{content} | |
</Link> | |
<button | |
ref={forwardedRef as Ref<HTMLButtonElement>} | |
{...(props as ButtonHTMLAttributes<HTMLButtonElement>)}> | |
{children} | |
</button> | |
); | |
} | |
return content; | |
} | |
); | |
ButtonOrLink.displayName = "ButtonOrLink"; | |
type ButtonOrLinkProps = | |
| ({ href: string } & AnchorHTMLAttributes<HTMLAnchorElement> & { children?: React.ReactNode }) | |
| ({ href?: never } & ButtonHTMLAttributes<HTMLButtonElement> & { children?: React.ReactNode }); | |
export const ButtonOrLink = forwardRef<HTMLAnchorElement | HTMLButtonElement, ButtonOrLinkProps>( | |
({ href, children, ...props }, forwardedRef) => { | |
if (href) { | |
return ( | |
<Link | |
href={href} | |
ref={forwardedRef as Ref<HTMLAnchorElement>} | |
{...props}> | |
{children} | |
</Link> | |
); | |
} | |
return ( | |
<button | |
ref={forwardedRef as Ref<HTMLButtonElement>} | |
{...props}> | |
{children} | |
</button> | |
); | |
} | |
); | |
ButtonOrLink.displayName = "ButtonOrLink"; |
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: 1
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (3)
ISSUE.md
(1 hunks)ISSUE_BODY.md
(1 hunks)PR_DESCRIPTION.md
(1 hunks)
✅ Files skipped from review due to trivial changes (1)
- PR_DESCRIPTION.md
🧰 Additional context used
🪛 markdownlint-cli2 (0.18.1)
ISSUE.md
3-3: Heading levels should only increment by one level at a time
Expected: h2; Actual: h3
(MD001, heading-increment)
⏰ Context from checks skipped due to timeout of 180000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
- GitHub Check: Type check / check-types
- GitHub Check: Linters / lint
- GitHub Check: Tests / Unit
- GitHub Check: Codacy Static Code Analysis
### Issue Summary | ||
One-sentence explanation: Next.js `Link` components using `legacyBehavior` caused DOM nesting, ref-forwarding and hydration/runtime errors (e.g. `null parentNode`); this PR migrates `Link` usage to the new API and fixes the related issues. | ||
|
||
--- | ||
|
||
### Steps to Reproduce | ||
1. Prepare the repository and dependencies: | ||
- `yarn install && yarn dedupe` | ||
2. Start the app in dev or production build: | ||
- `yarn dev` (or `yarn build && yarn start`) | ||
3. Open pages that use Link / interactive components such as: | ||
- Bookings single view, Forgot-password single view, AppNotInstalledMessage, Make & Zapier setup pages, pages with Dropdowns/Buttons-as-Links/List/Stepper | ||
4. Interact and observe: | ||
- Watch the browser console for hydration warnings and runtime errors when opening dropdowns or navigating via keyboard. | ||
5. Reproduce specific runtime error path: | ||
- Trigger flows that convert HTML → markdown (turndownService) or render server/client inconsistent Link markup to reproduce `Cannot read properties of null (reading 'parentNode')` or hydration mismatch. | ||
|
||
Any other relevant information: this is a bug because `legacyBehavior` produced inconsistent server vs client HTML structure and unsafe ref usage, causing developer-facing console warnings and potential user-facing UI breakage. Expected behavior is deterministic SSR → client render with correct element types, correct ref forwarding, and preserved accessibility (keyboard focus, ARIA). | ||
|
||
--- | ||
|
||
### Actual Results | ||
- Hydration mismatch warnings appear in the browser console on initial page load. | ||
- Runtime errors such as `Cannot read properties of null (reading 'parentNode')` from `turndownService` in certain flows. | ||
- Broken ref forwarding and nested interactive elements (anchor inside button or vice-versa) leading to unexpected keyboard behavior and accessibility regressions. | ||
- ESLint warnings related to unsafe `any` casts and unused imports in affected files. | ||
|
||
--- | ||
|
||
### Expected Results | ||
- No hydration mismatch warnings on page load. | ||
- No runtime exceptions from `turndownService` or component render paths. | ||
- Correct ref forwarding (use of `forwardRef` where needed) and no nested interactive elements. | ||
- Keyboard interaction and focus behavior remain correct and accessible. | ||
- Clean lint/type output for the modified files. | ||
|
||
--- | ||
|
||
### Technical details | ||
- Environment used for testing: macOS (local dev), Node.js 18+ recommended, Yarn | ||
- Commands used: | ||
- `yarn install && yarn dedupe` | ||
- `yarn dev` | ||
- `yarn build && yarn start` | ||
- `yarn lint` | ||
- `yarn test` | ||
- Branch with fixes: `fix/next-link-hydration` | ||
- Key files changed: | ||
- `packages/ui/components/dropdown/Dropdown.tsx` | ||
- `packages/ui/components/button/Button.tsx` | ||
- `packages/ui/components/list/List.tsx` | ||
- `packages/ui/components/form/step/Stepper.tsx` | ||
- `packages/lib/turndownService.ts` | ||
- `packages/features/shell/CalAiBanner.tsx` | ||
- `packages/app-store/_components/AppNotInstalledMessage.tsx` | ||
- `apps/web/modules/bookings/views/bookings-single-view.tsx` | ||
- `apps/web/components/apps/make/Setup.tsx` | ||
- `apps/web/components/apps/zapier/Setup.tsx` | ||
- `apps/web/modules/auth/forgot-password/[id]/forgot-password-single-view.tsx` | ||
|
||
--- | ||
|
||
### Evidence | ||
- Manual tests performed: | ||
- Dev and production builds: confirmed hydration warnings present before the change and resolved after migration to the new `Link` API. | ||
- Interacted with Dropdowns, List items, Buttons-as-Links, and Stepper to validate keyboard navigation and no nested anchors/buttons. | ||
- Reproduced and fixed the `null parentNode` error path in `turndownService`. | ||
- Attachments to include with the issue/PR: | ||
- Console screenshot showing hydration warning(s) (before) | ||
- Short GIF showing dropdown/button behavior before vs after (recommended) | ||
- Relevant console stack trace(s) for the runtime exception(s) | ||
- Testing notes: run the smoke test checklist in `PR_DESCRIPTION.md` and verify CI (typecheck/lint/tests) passes. |
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.
Fix heading hierarchy to satisfy markdownlint
After the top-level #
heading, the next heading jumps straight to ###
, triggering MD001. Please promote the section headings to H2 so the hierarchy increments by one level.
-### Issue Summary
+## Issue Summary
@@
-### Steps to Reproduce
+## Steps to Reproduce
@@
-### Actual Results
+## Actual Results
@@
-### Expected Results
+## Expected Results
@@
-### Technical details
+## Technical details
@@
-### Evidence
+## Evidence
📝 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.
### Issue Summary | |
One-sentence explanation: Next.js `Link` components using `legacyBehavior` caused DOM nesting, ref-forwarding and hydration/runtime errors (e.g. `null parentNode`); this PR migrates `Link` usage to the new API and fixes the related issues. | |
--- | |
### Steps to Reproduce | |
1. Prepare the repository and dependencies: | |
- `yarn install && yarn dedupe` | |
2. Start the app in dev or production build: | |
- `yarn dev` (or `yarn build && yarn start`) | |
3. Open pages that use Link / interactive components such as: | |
- Bookings single view, Forgot-password single view, AppNotInstalledMessage, Make & Zapier setup pages, pages with Dropdowns/Buttons-as-Links/List/Stepper | |
4. Interact and observe: | |
- Watch the browser console for hydration warnings and runtime errors when opening dropdowns or navigating via keyboard. | |
5. Reproduce specific runtime error path: | |
- Trigger flows that convert HTML → markdown (turndownService) or render server/client inconsistent Link markup to reproduce `Cannot read properties of null (reading 'parentNode')` or hydration mismatch. | |
Any other relevant information: this is a bug because `legacyBehavior` produced inconsistent server vs client HTML structure and unsafe ref usage, causing developer-facing console warnings and potential user-facing UI breakage. Expected behavior is deterministic SSR → client render with correct element types, correct ref forwarding, and preserved accessibility (keyboard focus, ARIA). | |
--- | |
### Actual Results | |
- Hydration mismatch warnings appear in the browser console on initial page load. | |
- Runtime errors such as `Cannot read properties of null (reading 'parentNode')` from `turndownService` in certain flows. | |
- Broken ref forwarding and nested interactive elements (anchor inside button or vice-versa) leading to unexpected keyboard behavior and accessibility regressions. | |
- ESLint warnings related to unsafe `any` casts and unused imports in affected files. | |
--- | |
### Expected Results | |
- No hydration mismatch warnings on page load. | |
- No runtime exceptions from `turndownService` or component render paths. | |
- Correct ref forwarding (use of `forwardRef` where needed) and no nested interactive elements. | |
- Keyboard interaction and focus behavior remain correct and accessible. | |
- Clean lint/type output for the modified files. | |
--- | |
### Technical details | |
- Environment used for testing: macOS (local dev), Node.js 18+ recommended, Yarn | |
- Commands used: | |
- `yarn install && yarn dedupe` | |
- `yarn dev` | |
- `yarn build && yarn start` | |
- `yarn lint` | |
- `yarn test` | |
- Branch with fixes: `fix/next-link-hydration` | |
- Key files changed: | |
- `packages/ui/components/dropdown/Dropdown.tsx` | |
- `packages/ui/components/button/Button.tsx` | |
- `packages/ui/components/list/List.tsx` | |
- `packages/ui/components/form/step/Stepper.tsx` | |
- `packages/lib/turndownService.ts` | |
- `packages/features/shell/CalAiBanner.tsx` | |
- `packages/app-store/_components/AppNotInstalledMessage.tsx` | |
- `apps/web/modules/bookings/views/bookings-single-view.tsx` | |
- `apps/web/components/apps/make/Setup.tsx` | |
- `apps/web/components/apps/zapier/Setup.tsx` | |
- `apps/web/modules/auth/forgot-password/[id]/forgot-password-single-view.tsx` | |
--- | |
### Evidence | |
- Manual tests performed: | |
- Dev and production builds: confirmed hydration warnings present before the change and resolved after migration to the new `Link` API. | |
- Interacted with Dropdowns, List items, Buttons-as-Links, and Stepper to validate keyboard navigation and no nested anchors/buttons. | |
- Reproduced and fixed the `null parentNode` error path in `turndownService`. | |
- Attachments to include with the issue/PR: | |
- Console screenshot showing hydration warning(s) (before) | |
- Short GIF showing dropdown/button behavior before vs after (recommended) | |
- Relevant console stack trace(s) for the runtime exception(s) | |
- Testing notes: run the smoke test checklist in `PR_DESCRIPTION.md` and verify CI (typecheck/lint/tests) passes. | |
## Issue Summary | |
One-sentence explanation: Next.js `Link` components using `legacyBehavior` caused DOM nesting, ref-forwarding and hydration/runtime errors (e.g. `null parentNode`); this PR migrates `Link` usage to the new API and fixes the related issues. | |
--- | |
## Steps to Reproduce | |
1. Prepare the repository and dependencies: | |
- `yarn install && yarn dedupe` | |
2. Start the app in dev or production build: | |
- `yarn dev` (or `yarn build && yarn start`) | |
3. Open pages that use Link / interactive components such as: | |
- Bookings single view, Forgot-password single view, AppNotInstalledMessage, Make & Zapier setup pages, pages with Dropdowns/Buttons-as-Links/List/Stepper | |
4. Interact and observe: | |
- Watch the browser console for hydration warnings and runtime errors when opening dropdowns or navigating via keyboard. | |
5. Reproduce specific runtime error path: | |
- Trigger flows that convert HTML → markdown (turndownService) or render server/client inconsistent Link markup to reproduce `Cannot read properties of null (reading 'parentNode')` or hydration mismatch. | |
Any other relevant information: this is a bug because `legacyBehavior` produced inconsistent server vs client HTML structure and unsafe ref usage, causing developer-facing console warnings and potential user-facing UI breakage. Expected behavior is deterministic SSR → client render with correct element types, correct ref forwarding, and preserved accessibility (keyboard focus, ARIA). | |
--- | |
## Actual Results | |
- Hydration mismatch warnings appear in the browser console on initial page load. | |
- Runtime errors such as `Cannot read properties of null (reading 'parentNode')` from `turndownService` in certain flows. | |
- Broken ref forwarding and nested interactive elements (anchor inside button or vice-versa) leading to unexpected keyboard behavior and accessibility regressions. | |
- ESLint warnings related to unsafe `any` casts and unused imports in affected files. | |
--- | |
## Expected Results | |
- No hydration mismatch warnings on page load. | |
- No runtime exceptions from `turndownService` or component render paths. | |
- Correct ref forwarding (use of `forwardRef` where needed) and no nested interactive elements. | |
- Keyboard interaction and focus behavior remain correct and accessible. | |
- Clean lint/type output for the modified files. | |
--- | |
## Technical details | |
- Environment used for testing: macOS (local dev), Node.js 18+ recommended, Yarn | |
- Commands used: | |
- `yarn install && yarn dedupe` | |
- `yarn dev` | |
- `yarn build && yarn start` | |
- `yarn lint` | |
- `yarn test` | |
- Branch with fixes: `fix/next-link-hydration` | |
- Key files changed: | |
- `packages/ui/components/dropdown/Dropdown.tsx` | |
- `packages/ui/components/button/Button.tsx` | |
- `packages/ui/components/list/List.tsx` | |
- `packages/ui/components/form/step/Stepper.tsx` | |
- `packages/lib/turndownService.ts` | |
- `packages/features/shell/CalAiBanner.tsx` | |
- `packages/app-store/_components/AppNotInstalledMessage.tsx` | |
- `apps/web/modules/bookings/views/bookings-single-view.tsx` | |
- `apps/web/components/apps/make/Setup.tsx` | |
- `apps/web/components/apps/zapier/Setup.tsx` | |
- `apps/web/modules/auth/forgot-password/[id]/forgot-password-single-view.tsx` | |
--- | |
## Evidence | |
- Manual tests performed: | |
- Dev and production builds: confirmed hydration warnings present before the change and resolved after migration to the new `Link` API. | |
- Interacted with Dropdowns, List items, Buttons-as-Links, and Stepper to validate keyboard navigation and no nested anchors/buttons. | |
- Reproduced and fixed the `null parentNode` error path in `turndownService`. | |
- Attachments to include with the issue/PR: | |
- Console screenshot showing hydration warning(s) (before) | |
- Short GIF showing dropdown/button behavior before vs after (recommended) | |
- Relevant console stack trace(s) for the runtime exception(s) | |
- Testing notes: run the smoke test checklist in `PR_DESCRIPTION.md` and verify CI (typecheck/lint/tests) passes. |
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)
3-3: Heading levels should only increment by one level at a time
Expected: h2; Actual: h3
(MD001, heading-increment)
🤖 Prompt for AI Agents
In ISSUE.md around lines 3 to 74, the heading hierarchy jumps from the top-level
H1 to H3 headings causing markdownlint MD001; update the section headings (e.g.,
"Issue Summary", "Steps to Reproduce", "Actual Results", "Expected Results",
"Technical details", "Evidence") from H3 (###) to H2 (##) so each heading level
increments by one after the initial H1, preserving existing text and
indentation.
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: 1
🧹 Nitpick comments (3)
packages/features/ee/managed-event-types/lib/handleChildrenEventTypes.ts (3)
1-1
: File-scoped ESLint disable is too broad; narrow itAvoid disabling rules for the whole file. After typing baseData and fixing Zod masks, you can drop this. If needed, use line-scoped disables just where unavoidable.
-/* eslint-disable @typescript-eslint/no-explicit-any, no-unsafe-optional-chaining */ +// Consider removing global disables and using line-scoped disables only where strictly needed.
122-136
: Removeas any
in Zod pick/omit; use a typed maskReplace the
as any
masks with a typed mask that aligns with the Zod object’s shape, or minimally add line-scoped disables.Example without
any
:const managedKeys = Object.keys(allManagedEventTypeProps) as Array<keyof typeof _ManagedEventTypeModel['shape']>; const managedMask = Object.fromEntries(managedKeys.map((k) => [k, true])) as { [K in keyof typeof _ManagedEventTypeModel['shape']]?: true; }; const allManagedEventTypePropsZod = _ManagedEventTypeModel.pick(managedMask); const managedEventTypeValues = allManagedEventTypePropsZod .omit({ locations: true, scheduleId: true, destinationCalendar: true }) // build unlocked mask explicitly .parse(eventType); const unlockedMask = { locations: true, scheduleId: true, destinationCalendar: true } as const; const unlockedEventTypeValues = allManagedEventTypePropsZod.pick(unlockedMask).parse(eventType);If keeping current approach, limit to:
// eslint-disable-next-line @typescript-eslint/no-explicit-any const allManagedEventTypePropsZod = _ManagedEventTypeModel.pick(allManagedEventTypeProps as any); // eslint-disable-next-line @typescript-eslint/no-explicit-any const managedEventTypeValues = allManagedEventTypePropsZod.omit(unlockedManagedEventTypeProps as any).parse(eventType); // eslint-disable-next-line @typescript-eslint/no-explicit-any const unlockedEventTypeValues = allManagedEventTypePropsZod.pick(unlockedManagedEventTypeProps as any).parse(eventType);
90-98
: Clean up unused param and prefer named export
_updatedValues
is unused. Remove it from the function params (and from the props type if not needed) to reduce noise.- Prefer named export over default, per repo guidelines. As per coding guidelines.
-export default async function handleChildrenEventTypes({ +export async function handleChildrenEventTypes({ eventTypeId: parentId, oldEventType, updatedEventType, children, prisma, profileId, - updatedValues: _updatedValues, }: handleChildrenEventTypesProps) {
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (2)
packages/features/ee/managed-event-types/lib/handleChildrenEventTypes.ts
(5 hunks)packages/ui/components/address/Select.tsx
(3 hunks)
✅ Files skipped from review due to trivial changes (1)
- packages/ui/components/address/Select.tsx
🧰 Additional context used
📓 Path-based instructions (3)
**/*.ts
📄 CodeRabbit inference engine (.cursor/rules/review.mdc)
**/*.ts
: For Prisma queries, only select data you need; never useinclude
, always useselect
Ensure thecredential.key
field is never returned from tRPC endpoints or APIs
Files:
packages/features/ee/managed-event-types/lib/handleChildrenEventTypes.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/review.mdc)
Flag excessive Day.js use in performance-critical code; prefer native Date or Day.js
.utc()
in hot paths like loops
Files:
packages/features/ee/managed-event-types/lib/handleChildrenEventTypes.ts
**/*.{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/features/ee/managed-event-types/lib/handleChildrenEventTypes.ts
🧠 Learnings (1)
📚 Learning: 2025-09-12T07:15:58.056Z
Learnt from: hbjORbj
PR: calcom/cal.com#23475
File: packages/features/credentials/handleDeleteCredential.ts:5-10
Timestamp: 2025-09-12T07:15:58.056Z
Learning: When EventTypeAppMetadataSchema is used only in z.infer<typeof EventTypeAppMetadataSchema> type annotations or type casts, it can be imported as a type-only import since this usage is purely at the TypeScript type level and doesn't require the runtime value.
Applied to files:
packages/features/ee/managed-event-types/lib/handleChildrenEventTypes.ts
🧬 Code graph analysis (1)
packages/features/ee/managed-event-types/lib/handleChildrenEventTypes.ts (1)
packages/prisma/zod-utils.ts (2)
allManagedEventTypeProps
(627-696)unlockedManagedEventTypeProps
(700-704)
⏰ Context from checks skipped due to timeout of 180000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
- GitHub Check: Tests / Unit
- GitHub Check: Type check / check-types
- GitHub Check: Linters / lint
- GitHub Check: Codacy Static Code Analysis
// build data object to avoid duplicate property definitions in object literal | ||
data: (() => { | ||
const baseData: any = { | ||
profileId: profileId ?? null, | ||
...managedValuesWithoutExplicit, | ||
...unlockedEventTypeValues, | ||
// pre-genned as allowed null | ||
locations: Array.isArray(unlockedEventTypeValues.locations) | ||
? unlockedEventTypeValues.locations | ||
: undefined, | ||
}, | ||
bookingLimits: | ||
(managedEventTypeValues.bookingLimits as unknown as Prisma.InputJsonObject) ?? undefined, | ||
recurringEvent: | ||
(managedEventTypeValues.recurringEvent as unknown as Prisma.InputJsonValue) ?? undefined, | ||
metadata: (managedEventTypeValues.metadata as Prisma.InputJsonValue) ?? undefined, | ||
bookingFields: (managedEventTypeValues.bookingFields as Prisma.InputJsonValue) ?? undefined, | ||
durationLimits: (managedEventTypeValues.durationLimits as Prisma.InputJsonValue) ?? undefined, | ||
eventTypeColor: (managedEventTypeValues.eventTypeColor as Prisma.InputJsonValue) ?? undefined, | ||
onlyShowFirstAvailableSlot: managedEventTypeValues.onlyShowFirstAvailableSlot ?? false, | ||
userId, | ||
users: { | ||
connect: [{ id: userId }], | ||
}, | ||
parentId, | ||
hidden: children?.find((ch) => ch.owner.id === userId)?.hidden ?? false, | ||
workflows: currentWorkflowIds && { | ||
create: currentWorkflowIds.map((wfId) => ({ workflowId: wfId })), | ||
}, | ||
/** | ||
* RR Segment isn't applicable for managed event types. | ||
*/ | ||
rrSegmentQueryValue: undefined, | ||
assignRRMembersUsingSegment: false, | ||
useEventLevelSelectedCalendars: false, | ||
restrictionScheduleId: null, | ||
useBookerTimezone: false, | ||
allowReschedulingCancelledBookings: | ||
managedEventTypeValues.allowReschedulingCancelledBookings ?? false, | ||
}, | ||
bookingLimits: | ||
(managedEventTypeValues.bookingLimits as unknown as Prisma.InputJsonObject) ?? undefined, | ||
recurringEvent: | ||
(managedEventTypeValues.recurringEvent as unknown as Prisma.InputJsonValue) ?? undefined, | ||
metadata: (managedEventTypeValues.metadata as Prisma.InputJsonValue) ?? undefined, | ||
bookingFields: (managedEventTypeValues.bookingFields as Prisma.InputJsonValue) ?? undefined, | ||
durationLimits: (managedEventTypeValues.durationLimits as Prisma.InputJsonValue) ?? undefined, | ||
eventTypeColor: (managedEventTypeValues.eventTypeColor as Prisma.InputJsonValue) ?? undefined, | ||
onlyShowFirstAvailableSlot: managedEventTypeValues.onlyShowFirstAvailableSlot ?? false, | ||
userId, | ||
users: { | ||
connect: [{ id: userId }], | ||
}, | ||
parentId, | ||
hidden: children?.find((ch) => ch.owner.id === userId)?.hidden ?? false, | ||
workflows: currentWorkflowIds && { | ||
create: currentWorkflowIds.map((wfId) => ({ workflowId: wfId })), | ||
}, | ||
/** | ||
* RR Segment isn't applicable for managed event types. | ||
*/ | ||
rrSegmentQueryValue: undefined, | ||
assignRRMembersUsingSegment: false, | ||
useEventLevelSelectedCalendars: false, | ||
restrictionScheduleId: null, | ||
useBookerTimezone: false, | ||
allowReschedulingCancelledBookings: | ||
managedEventTypeValues.allowReschedulingCancelledBookings ?? false, | ||
}; | ||
// set instantMeetingScheduleId explicitly after building baseData to avoid duplicate definitions | ||
baseData.instantMeetingScheduleId = eventType.instantMeetingScheduleId ?? undefined; | ||
return baseData; | ||
})(), | ||
}); |
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.
🧩 Analysis chain
instantMeetingScheduleId is never loaded; create payload always sets it to undefined
You set baseData.instantMeetingScheduleId from eventType, but eventType is fetched with select: allManagedEventTypeProps, which doesn’t include instantMeetingScheduleId. Result: field is always undefined in creates.
Fix by selecting it explicitly (keep it out of managed picks) and type the payload:
TypeScript (outside this hunk):
// ensure it's selected
const eventType = await prisma.eventType.findFirst({
where: { id: parentId },
select: { ...allManagedEventTypeProps, instantMeetingScheduleId: true },
});
Apply within this hunk:
- const baseData: any = {
+ const baseData: Prisma.EventTypeCreateInput = {
profileId: profileId ?? null,
...managedValuesWithoutExplicit,
...unlockedEventTypeValues,
// pre-genned as allowed null
locations: Array.isArray(unlockedEventTypeValues.locations)
? unlockedEventTypeValues.locations
: undefined,
@@
- workflows: currentWorkflowIds && {
- create: currentWorkflowIds.map((wfId) => ({ workflowId: wfId })),
- },
+ workflows: currentWorkflowIds?.length
+ ? { create: currentWorkflowIds.map((wfId) => ({ workflowId: wfId })) }
+ : undefined,
@@
- baseData.instantMeetingScheduleId = eventType.instantMeetingScheduleId ?? undefined;
+ baseData.instantMeetingScheduleId = eventType.instantMeetingScheduleId ?? undefined;
Verification script to confirm the field exists in the repo:
🏁 Script executed:
#!/bin/bash
# Look for the field in Prisma model and usages
rg -nP -C2 '\binstantMeetingScheduleId\b'
Length of output: 13202
🏁 Script executed:
#!/bin/bash
rg -n 'allManagedEventTypeProps' -C5
Length of output: 12699
🏁 Script executed:
#!/bin/bash
rg -n 'eslint-disable' -C2 packages/features/ee/managed-event-types/lib/handleChildrenEventTypes.ts
Length of output: 151
🏁 Script executed:
#!/bin/bash
rg -n 'export const allManagedEventTypeProps' -A200 packages/prisma/zod-utils.ts
Length of output: 7243
Select and propagate instantMeetingScheduleId in the create payload
The eventType
is fetched with select: allManagedEventTypeProps
, which doesn’t include instantMeetingScheduleId
, so baseData.instantMeetingScheduleId
is always undefined. Include it explicitly and tighten your typings:
// ensure it’s selected
const eventType = await prisma.eventType.findFirst({
where: { id: parentId },
select: { ...allManagedEventTypeProps, instantMeetingScheduleId: true },
});
- const baseData: any = {
+ const baseData: Prisma.EventTypeCreateInput = {
profileId: profileId ?? null,
…
- workflows: currentWorkflowIds && {
- create: currentWorkflowIds.map((wfId) => ({ workflowId: wfId })),
- },
+ workflows: currentWorkflowIds?.length
+ ? { create: currentWorkflowIds.map((wfId) => ({ workflowId: wfId })) }
+ : undefined,
};
- baseData.instantMeetingScheduleId = eventType.instantMeetingScheduleId ?? undefined;
+ baseData.instantMeetingScheduleId = eventType.instantMeetingScheduleId ?? undefined;
🤖 Prompt for AI Agents
In packages/features/ee/managed-event-types/lib/handleChildrenEventTypes.ts
around lines 170 to 213, the code assigns baseData.instantMeetingScheduleId from
eventType but the earlier prisma query did not select instantMeetingScheduleId
so it will always be undefined; update the prisma.eventType.findFirst/select to
include instantMeetingScheduleId: true (e.g., spread allManagedEventTypeProps
and add instantMeetingScheduleId: true), and tighten the TypeScript typings for
the fetched eventType to include instantMeetingScheduleId (and handle possible
null eventType) so baseData.instantMeetingScheduleId can be populated correctly.
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.
Please fix yarn.lock. Too many things removed from it.
Also please remove the ISSUE.md, ISSUE_BODY.md and PR_DESCRIPTION.md files
Fixes #24417
Summary
Link
legacyBehavior
usage across the codebase and migrated affected components to the newLink
API to resolve ref-forwarding, DOM nesting, and hydration/runtime errors observed while running the application locally.null parentNode
), improved SSR determinism, and preserved accessibility/interactive behavior for core UI components (Dropdown, Button, List, Stepper, etc.).Impact / Scope
Motivation
Using
legacyBehavior
produced inconsistent server/client markup, broken refs, and nested interactive elements (anchor inside button), leading to hydration warnings and runtime exceptions. Migrating to the newLink
API prevents these problems and prepares the codebase for React 19 / Next.js 15.Files changed (high level)
packages/ui/components/dropdown/Dropdown.tsx
packages/ui/components/button/Button.tsx
packages/ui/components/list/List.tsx
packages/ui/components/form/step/Stepper.tsx
packages/lib/turndownService.ts
packages/features/shell/CalAiBanner.tsx
packages/app-store/_components/AppNotInstalledMessage.tsx
apps/web/modules/bookings/views/bookings-single-view.tsx
apps/web/components/apps/make/Setup.tsx
apps/web/components/apps/zapier/Setup.tsx
apps/web/modules/auth/forgot-password/[id]/forgot-password-single-view.tsx
Key changes
legacyBehavior
and updated Link usage to the new API (passclassName
/props directly toLink
).any
casts in Dropdown and Button components.ListLinkItem
to avoid nested interactive elements.turndownService
to avoid traversing null parent nodes.CalAiBanner
visibility to client mount to prevent hydration mismatches.Manual QA steps (recommended)
yarn install && yarn dedupe
yarn dev
# or build withyarn build
yarn lint
yarn test
null parentNode
, ref issues) no longer appear in logs/consoleWhat reviewers should verify
fix:
) — required for CIlegacyBehavior
any
casts)Commands (copy to terminal)
Update PR title (run once)
gh pr edit 24417 --title "fix(ui): migrate off Next.js — fix hydration & ref-forwarding issues"
Update PR body from this file (run once)
gh pr edit 24417 --body-file PR_DESCRIPTION.md
Optional: add reviewers/labels
gh pr edit 24417 --add-reviewer maintainer1 --add-reviewer maintainer2 --add-label "area:ui" --add-label "type:bug"
Local checks
yarn install && yarn dedupe
yarn build
yarn lint
yarn test
Visual demo (strongly recommended)
Checklist (DO NOT REMOVE)
Notes