Skip to content

upcoming: [UIE-9823] - Implement the Contact Sales Drawer for Marketplace products#13368

Merged
harsh-akamai merged 28 commits intolinode:developfrom
harsh-akamai:UIE-9823
Feb 12, 2026
Merged

upcoming: [UIE-9823] - Implement the Contact Sales Drawer for Marketplace products#13368
harsh-akamai merged 28 commits intolinode:developfrom
harsh-akamai:UIE-9823

Conversation

@harsh-akamai
Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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()}

} from '@linode/ui';
import { createPartnerReferralSchema } from '@linode/validation';
import { createFilterOptions, Grid } from '@mui/material';
import { enqueueSnackbar } from 'notistack';
Copy link
Copy Markdown
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
Copy Markdown
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

<MultipleIPInput
buttonText="Add email address"
className={
field.value?.length === 2
Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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.

*/
renderVariant?: () => JSX.Element | null;
/**
* An optional prop to set the ARIA role of the selection card.
Copy link
Copy Markdown
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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 2660f98

}}
>
Load More...
Load more offers &gt;
Copy link
Copy Markdown
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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 2660f98

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
Copy Markdown
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?

Copy link
Copy Markdown
Contributor Author

@harsh-akamai harsh-akamai Feb 11, 2026

Choose a reason for hiding this comment

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

screenshot image

The slotProps help set the width for the options dropdown.
I've used media queries because the drawer width is set to 445px for small screens. Using breakpoints here wouldn't really help once we reach screen sizes less than the drawer width.

}}
options={countryList}
placeholder=""
renderOption={({ key, ...props }, option) => (
Copy link
Copy Markdown
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

@harsh-akamai harsh-akamai requested a review from a team as a code owner February 11, 2026 07:29
@harsh-akamai harsh-akamai requested review from shagufa-akamai and removed request for a team February 11, 2026 07:29
Copy link
Copy Markdown
Contributor

@pmakode-akamai pmakode-akamai left a comment

Choose a reason for hiding this comment

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

thanks @harsh-akamai! Since we made the ARAI role prop optional (with no default) in SelectionCard, I think we'll still need to pass role="button" from ProductSelectionCard.tsx just for this case

@github-project-automation github-project-automation bot moved this from Review to Approved in Cloud Manager Feb 12, 2026
@linode-gh-bot
Copy link
Copy Markdown
Collaborator

Cloud Manager UI test results

🔺 2 failing tests on test run #24 ↗︎

❌ Failing✅ Passing↪️ Skipped🕐 Duration
2 Failing864 Passing11 Skipped42m 38s

Details

Failing Tests
SpecTest
rebuild-linode.spec.tsCloud Manager Cypress Tests→rebuild linode » rebuilds a linode from Account StackScript
timerange-verification.spec.tsCloud Manager Cypress Tests→Integration tests for verifying Cloudpulse custom and preset configurations » should implement and validate custom date/time picker for a specific date and time range

Troubleshooting

Use this command to re-run the failing tests:

pnpm cy:run -s "cypress/e2e/core/linodes/rebuild-linode.spec.ts,cypress/e2e/core/cloudpulse/timerange-verification.spec.ts"

@harsh-akamai harsh-akamai merged commit 833e4cd into linode:develop Feb 12, 2026
34 of 35 checks passed
@github-project-automation github-project-automation bot moved this from Approved to Merged in Cloud Manager Feb 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

5 participants