Skip to content

Conversation

@harsh-akamai
Copy link
Contributor

@harsh-akamai harsh-akamai commented Feb 5, 2026

Description 📝

Implement the Contact Sales Drawer for Marketplace products

Scope 🚢

Upon production release, changes in this PR will be visible to:

  • All customers
  • Some customers (e.g. in Beta or Limited Availability)
  • No customers / Not applicable

Preview 📷

Non Error State Error State
image image

How to test 🧪

Prerequisites

Verification steps

  • Turn on the MarketplaceV2 feature flag
  • Navigate to localhost:3000/cloud-marketplace/catalog/{productName} (or go to the "Partner Referrals" page and click one of the cards)
  • Click on the "Contact Sales" button
  • Verify that the form styles stay responsive in different layouts
  • Ensure the error states are thrown correctly
  • Click on "Submit" and verify that you get a success or error snackbar
Author Checklists

As an Author, to speed up the review process, I considered 🤔

👀 Doing a self review
❔ Our contribution guidelines
🤏 Splitting feature into small PRs
➕ Adding a changeset
🧪 Providing/improving test coverage
🔐 Removing all sensitive information from the code and PR description
🚩 Using a feature flag to protect the release
👣 Providing comprehensive reproduction steps
📑 Providing or updating our documentation
🕛 Scheduling a pair reviewing session
📱 Providing mobile support
♿ Providing accessibility support


  • I have read and considered all applicable items listed above.

As an Author, before moving this PR from Draft to Open, I confirmed ✅

  • All tests and CI checks are passing
  • TypeScript compilation succeeded without errors
  • Code passes all linting rules

@harsh-akamai harsh-akamai marked this pull request as ready for review February 5, 2026 06:22
@harsh-akamai harsh-akamai requested a review from a team as a code owner February 5, 2026 06:23
@tvijay-akamai
Copy link
Contributor

tvijay-akamai commented Feb 8, 2026

The form initializes additional email to ['']. API Spec expects: ["email1@akamai.com"] or omit if empty
What gets sent: ['']. Please check with backend team if this is okay.

@tvijay-akamai
Copy link
Contributor

One observation in api spec doc there is a typo API Spec shows: "parner_name": "Akamai" (typo - missing 't'). Please get this corrected from backend team.

? getAPIErrorOrDefault(error)?.[0].reason
: "Oops! Something went wrong and we couldn't send your contacts. Please try again in a moment, or refresh the page.";
enqueueSnackbar(errorMessage, { variant: 'error' });
onClose();
Copy link
Contributor

Choose a reason for hiding this comment

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

On api error do we need to close the drawer?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The snackbar will only be shown if we close the drawer.

Copy link
Contributor

Choose a reason for hiding this comment

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

We might not want to close the drawer on API errors since that would require the user to re-enter their details, which isn't ideal. Typically, in CM, we don't close drawers when an API call fails; instead, we usually show an error notification banner within the drawer itself

I also noticed that form data persists when reopening the drawer, but navigating between tabs within the details page resets the form data. I think the drawer should always open with an empty state

return (
<Drawer
data-testid="contact-sales-drawer"
onClose={() => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of resetting the form onClose which can cause visual glitches if the form resets while the drawer is animating closed. we can use onTransitionExited={() => reset()}

@@ -0,0 +1,590 @@
import { yupResolver } from '@hookform/resolvers/yup';
Copy link
Contributor

Choose a reason for hiding this comment

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

No unit tests for ContactSalesDrawer component

} from '@linode/ui';
import { createPartnerReferralSchema } from '@linode/validation';
import { createFilterOptions, Grid } from '@mui/material';
import { enqueueSnackbar } from 'notistack';
Copy link
Contributor

Choose a reason for hiding this comment

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

mostly this pattern is follwed in the codebase import { useSnackbar } from 'notistack';
const { enqueueSnackbar } = useSnackbar(); . Direct import works but is inconsistent with codebase conventions.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I believe both the implementations are supported. Importing enqueueSnackBar has been followed in multiple places like here: https://github.com/linode/manager/blob/develop/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeEntityDetailFooter.tsx#L6

render={({ field }) => {
const emailErrors = errors.additional_emails;
return (
<MultipleIPInput
Copy link
Contributor

Choose a reason for hiding this comment

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

Using MultipleIPInput for email addresses works but is semantically confusing. looked like the component was designed for IP addresses. Consider:

Adding a comment explaining this reuse
Or see if there is possibility of MultipleEmailInput wrapper

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We did agree that building a custom component for this use case is not justified as of yet. Will add a comment for clarity though

<MultipleIPInput
buttonText="Add email address"
className={
field.value?.length === 2
Copy link
Contributor

Choose a reason for hiding this comment

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

can we use a constant here : MAX_ADDITIONAL_EMAILS = 2


const {
control,
formState: { errors, isSubmitting },
Copy link
Contributor

Choose a reason for hiding this comment

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

When the form is submitted While isSubmitting is used to disable form fields. Is this is correct submitting state. Can we check with ux on this.

const { classes } = useStyles();
const { onClose, open, partnerName, productName } = props;

const { data: profile } = useProfile();
Copy link
Contributor

Choose a reason for hiding this comment

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

Are we sure this data is not asynchronous and will always be available when drawer is opened? Basically do you this we should handle async nature of this data?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The useProfile() api is loaded on login so we will always have the data.

@@ -0,0 +1,590 @@
import { yupResolver } from '@hookform/resolvers/yup';
Copy link
Contributor

Choose a reason for hiding this comment

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

Missing Notice for in-form api returned errors

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The API needs to send the field parameter in errors if we want to show different error components for in-form errors. Currently, the error response from this api doesn't return this field parameter

open={open}
title={`Contact ${partnerName} sales`}
>
<form onSubmit={onSubmit}>
Copy link
Contributor

Choose a reason for hiding this comment

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

Pendo id's missing on all the elements. Please add attributes as mentioned in figma provided by pendo team.

id={id}
onClick={handleClick}
onKeyPress={handleKeyPress}
role={role}
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
role={role}
role={role ?? (onClick ? 'button' : undefined)}

I think it would be better to automatically set role="button" when onClick is provided, rather than requiring consumers to manually pass a role. This helps ensure that clickable cards are exposed as interactive to screen readers by default while still allowing flexibility to override the role when different semantics are needed (for eg., radio-button behavior with onClick)

heading={productName}
onClick={onClick}
renderIcon={renderHeader}
role="button"
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
role="button"

secondaryButtonProps={{
label: 'Cancel',
onClick: onClose,
buttonType: 'outlined',
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
buttonType: 'outlined',

We should keep the secondary button consistent across CM (without the outlined variant) until outlined styling is handled as the default as part of the CDS migration work

Image

? getAPIErrorOrDefault(error)?.[0].reason
: "Oops! Something went wrong and we couldn't send your contacts. Please try again in a moment, or refresh the page.";
enqueueSnackbar(errorMessage, { variant: 'error' });
onClose();
Copy link
Contributor

Choose a reason for hiding this comment

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

We might not want to close the drawer on API errors since that would require the user to re-enter their details, which isn't ideal. Typically, in CM, we don't close drawers when an API call fails; instead, we usually show an error notification banner within the drawer itself

I also noticed that form data persists when reopening the drawer, but navigating between tabs within the details page resets the form data. I think the drawer should always open with an empty state

Copy link
Contributor

@dwiley-akamai dwiley-akamai left a comment

Choose a reason for hiding this comment

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

We'll need an Upcoming Features changeset for each package touched

}
});

it('renders an removable text field on click of the "Add Email Address" button', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
it('renders an removable text field on click of the "Add Email Address" button', async () => {
it('renders a removable text field on click of the "Add Email Address" button', async () => {

Comment on lines +175 to +176
Fill the form and our partner&apos;s sales team will reach out to
you
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't this have a period at the end? And maybe something like "Complete the form [...]" would make it sound a bit more formal

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was the phrasing preferred by the tech writing team. Will double check on this

@linode-gh-bot
Copy link
Collaborator

Cloud Manager UI test results

🔺 1 failing test on test run #12 ↗︎

❌ Failing✅ Passing↪️ Skipped🕐 Duration
1 Failing865 Passing11 Skipped37m 41s

Details

Failing Tests
SpecTest
firewall-landing-page.spec.tsCloud Manager Cypress Tests→confirms Firewalls landing page empty state is shown when no Firewalls exist » lists all Firewalls

Troubleshooting

Use this command to re-run the failing tests:

pnpm cy:run -s "cypress/e2e/core/firewalls/firewall-landing-page.spec.ts"

*/
renderVariant?: () => JSX.Element | null;
/**
* An optional prop to set the ARIA role of the selection card.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* An optional prop to set the ARIA role of the selection card.
* An optional prop to set the ARIA role of the selection card.

Very minor nit: extra spacing between words

}}
>
Load More...
Load more offers &gt;
Copy link
Contributor

Choose a reason for hiding this comment

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

We should use chevron-right icon here

Comment on lines +356 to +371
slotProps={{
paper: {
sx: {
overflow: 'hidden',
// Set the options width to cover the entire textfield when the drawer is at or above its designed width
width: {
sm: '401px',
xs: '366px',
},
// When the drawer width is less than 445px, expand to drawer width (minus padding offset) on mobile
'@media (max-width: 445px)': {
width: 'calc(100vw - 79px)',
},
},
},
}}
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
slotProps={{
paper: {
sx: {
overflow: 'hidden',
// Set the options width to cover the entire textfield when the drawer is at or above its designed width
width: {
sm: '401px',
xs: '366px',
},
// When the drawer width is less than 445px, expand to drawer width (minus padding offset) on mobile
'@media (max-width: 445px)': {
width: 'calc(100vw - 79px)',
},
},
},
}}

I don't see any effect from these slotProps styles on the phone number field. We usually avoid using media queries directly like this and prefer breakpoints instead. Since I didn't notice any visible impact from the above, this might not be required at all. Could you double-check?

}}
options={countryList}
placeholder=""
renderOption={({ key, ...props }, option) => (
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like this key prop is unused here

}}
title="Additional email addresses"
tooltip="You can add two additional emails"
/>
Copy link
Contributor

@pmakode-akamai pmakode-akamai Feb 10, 2026

Choose a reason for hiding this comment

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

Currently, this field doesn't span the full width to align with the other fields. Should we adjust it to match the UX mockups?

Image

control={control}
name="phone_country_code"
render={({ field }) => (
<Autocomplete
Copy link
Contributor

Choose a reason for hiding this comment

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

Screen.Recording.2026-02-10.at.6.42.44.PM.mov

I noticed a slight pixel shift when hovering over this input field. Could we fix this?

control={control}
name="country_code"
render={({ field }) => (
<Autocomplete
Copy link
Contributor

Choose a reason for hiding this comment

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

I've noticed this console error locally (not on preview link) when region is selected from the list.

Image

Also, this field persists the selected value every time the form is reopened - I believe It should have no value initially when the form is opened.

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

Projects

Status: Review

Development

Successfully merging this pull request may close these issues.

5 participants