Skip to content

Commit

Permalink
feat: [M3-8336] - Improve support ticket form pre-population and fiel…
Browse files Browse the repository at this point in the history
…d labels (#10745)

* Pass form payload fields to support ticket form

* Display Linode plan label; fix ticket title regression

* Display LKE plan label(s)

* Clean up; fix type not being passed in create flow v1

* Fix console error for uncontrolled value; fix validation bug

* Update test

* Improve the entity type labeling

* Update test to handle dynamic entity type label

* Fix default value ordering

* Added changeset: Improve support ticket form  pre-population and field labels
  • Loading branch information
mjac0bs authored Aug 20, 2024
1 parent 4a9839b commit 81082b0
Show file tree
Hide file tree
Showing 15 changed files with 211 additions and 69 deletions.
5 changes: 5 additions & 0 deletions packages/manager/.changeset/pr-10745-changed-1723666784731.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Changed
---

Improve support ticket form pre-population and field labels ([#10745](https://github.com/linode/manager/pull/10745))
Original file line number Diff line number Diff line change
Expand Up @@ -365,14 +365,14 @@ describe('open support tickets', () => {
description: '',
entityId: '',
entityInputValue: '',
entityType: 'general' as EntityType,
entityType: 'linode_id' as EntityType,
selectedSeverity: undefined,
summary: 'Account Limit Increase',
ticketType: 'accountLimit' as TicketType,
companyName: mockAccount.company,
customerName: `${mockAccount.first_name} ${mockAccount.last_name}`,
companyName: mockAccount.company,
numberOfEntities: '2',
linodePlan: 'Nanode 1 GB',
linodePlan: 'Nanode 1GB',
useCase: randomString(),
publicInfo: randomString(),
};
Expand Down Expand Up @@ -449,6 +449,13 @@ describe('open support tickets', () => {
.should('be.visible')
.should('have.value', mockFormFields.companyName);

// Confirm plan pre-populates from form payload data.
cy.findByLabelText('Which Linode plan do you need access to?', {
exact: false,
})
.should('be.visible')
.should('have.value', mockFormFields.linodePlan);

// Confirm helper text and link.
cy.findByText('Current number of Linodes: 1').should('be.visible');
cy.findByText('View types of plans')
Expand All @@ -467,16 +474,11 @@ describe('open support tickets', () => {
cy.findByText('Links to public information are required.');

// Complete the rest of the form.
cy.findByLabelText('Total number of entities you need?')
cy.findByLabelText('Total number of Linodes you need?')
.should('be.visible')
.click()
.type(mockFormFields.numberOfEntities);

cy.findByLabelText('Which Linode plan do you need access to?')
.should('be.visible')
.click()
.type(mockFormFields.linodePlan);

cy.get('[data-qa-ticket-use-case]')
.should('be.visible')
.click()
Expand Down Expand Up @@ -511,9 +513,13 @@ describe('open support tickets', () => {
cy.contains(
`#${mockAccountLimitTicket.id}: ${mockAccountLimitTicket.summary}`
).should('be.visible');
Object.values(ACCOUNT_LIMIT_FIELD_NAME_TO_LABEL_MAP).forEach(
(fieldLabel) => {
cy.findByText(fieldLabel).should('be.visible');
Object.entries(ACCOUNT_LIMIT_FIELD_NAME_TO_LABEL_MAP).forEach(
([key, fieldLabel]) => {
let _fieldLabel = fieldLabel;
if (key === 'useCase' || key === 'numberOfEntities') {
_fieldLabel = _fieldLabel.replace('entities', 'Linodes');
}
cy.findByText(_fieldLabel).should('be.visible');
}
);
});
Expand Down
9 changes: 7 additions & 2 deletions packages/manager/src/components/ErrorMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,28 @@ import React from 'react';
import { SupportTicketGeneralError } from './SupportTicketGeneralError';
import { Typography } from './Typography';

import type { EntityType } from 'src/features/Support/SupportTickets/SupportTicketDialog';
import type {
EntityType,
FormPayloadValues,
} from 'src/features/Support/SupportTickets/SupportTicketDialog';

interface Props {
entityType: EntityType;
formPayloadValues?: FormPayloadValues;
message: string;
}

export const supportTextRegex = /(open a support ticket|contact Support)/i;

export const ErrorMessage = (props: Props) => {
const { entityType, message } = props;
const { entityType, formPayloadValues, message } = props;
const isSupportTicketError = supportTextRegex.test(message);

if (isSupportTicketError) {
return (
<SupportTicketGeneralError
entityType={entityType}
formPayloadValues={formPayloadValues}
generalError={message}
/>
);
Expand Down
14 changes: 13 additions & 1 deletion packages/manager/src/components/SupportLink/SupportLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import { Link } from 'react-router-dom';
import type { LinkProps } from 'react-router-dom';
import type {
EntityType,
FormPayloadValues,
TicketType,
} from 'src/features/Support/SupportTickets/SupportTicketDialog';

interface SupportLinkProps {
description?: string;
entity?: EntityForTicketDetails;
formPayloadValues?: FormPayloadValues;
onClick?: LinkProps['onClick'];
text: string;
ticketType?: TicketType;
Expand All @@ -22,14 +24,24 @@ export interface EntityForTicketDetails {
}

const SupportLink = (props: SupportLinkProps) => {
const { description, entity, onClick, text, ticketType, title } = props;
const {
description,
entity,
formPayloadValues,
onClick,
text,
ticketType,
title,
} = props;

return (
<Link
to={{
pathname: '/support/tickets',
state: {
description,
entity,
formPayloadValues,
open: true,
ticketType,
title,
Expand Down
9 changes: 7 additions & 2 deletions packages/manager/src/components/SupportTicketGeneralError.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@ import { capitalize } from 'src/utilities/capitalize';
import { supportTextRegex } from './ErrorMessage';
import { Typography } from './Typography';

import type { EntityType } from 'src/features/Support/SupportTickets/SupportTicketDialog';
import type {
EntityType,
FormPayloadValues,
} from 'src/features/Support/SupportTickets/SupportTicketDialog';

interface SupportTicketGeneralErrorProps {
entityType: EntityType;
formPayloadValues?: FormPayloadValues;
generalError: string;
}

Expand All @@ -19,7 +23,7 @@ const accountLimitRegex = /(limit|limit for the number of active services) on yo
export const SupportTicketGeneralError = (
props: SupportTicketGeneralErrorProps
) => {
const { entityType, generalError } = props;
const { entityType, formPayloadValues, generalError } = props;
const theme = useTheme();

const limitError = generalError.split(supportTextRegex);
Expand Down Expand Up @@ -50,6 +54,7 @@ export const SupportTicketGeneralError = (
isAccountLimitSupportTicket ? 'accountLimit' : 'general'
}
entity={{ id: undefined, type: entityType }}
formPayloadValues={formPayloadValues}
key={`${substring}-${idx}`}
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,11 @@ export const CreateCluster = () => {
<Grid className={`mlMain py0`}>
{generalError && (
<Notice variant="error">
<ErrorMessage entityType="lkecluster_id" message={generalError} />
<ErrorMessage
entityType="lkecluster_id"
formPayloadValues={{ node_pools: nodePools }}
message={generalError}
/>
</Notice>
)}
<Paper data-qa-label-header>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ import type { CreateLinodeRequest } from '@linode/api-v4';
export const Error = () => {
const {
formState: { errors },
getValues,
} = useFormContext<CreateLinodeRequest>();

const generalError = errors.root?.message ?? errors.interfaces?.message;
const values = getValues();

if (!generalError) {
return null;
Expand All @@ -21,7 +23,11 @@ export const Error = () => {
return (
<Paper sx={{ p: 0 }}>
<Notice spacingBottom={0} spacingTop={0} variant="error">
<ErrorMessage entityType="linode_id" message={generalError} />
<ErrorMessage
entityType="linode_id"
formPayloadValues={{ type: values.type }}
message={generalError}
/>
</Notice>
</Paper>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -846,7 +846,11 @@ export class LinodeCreate extends React.PureComponent<
)}
{generalError && (
<Notice spacingTop={8} variant="error">
<ErrorMessage entityType="linode_id" message={generalError} />
<ErrorMessage
entityType="linode_id"
formPayloadValues={{ type: this.props.selectedTypeID }}
message={generalError}
/>
</Notice>
)}
{userCannotCreateLinode && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,71 @@ import { Controller, useFormContext } from 'react-hook-form';
import { Link } from 'src/components/Link';
import { TextField } from 'src/components/TextField';
import { useAccount } from 'src/queries/account/account';
import { useSpecificTypes, useTypeQuery } from 'src/queries/types';
import { extendTypesQueryResult } from 'src/utilities/extendType';

import { ACCOUNT_LIMIT_FIELD_NAME_TO_LABEL_MAP } from './constants';
import { SupportTicketProductSelectionFields } from './SupportTicketProductSelectionFields';
import { getEntityNameFromEntityType } from './ticketUtils';

import type { CustomFields } from './constants';
import type { SupportTicketFormFields } from './SupportTicketDialog';
import type {
FormPayloadValues,
SupportTicketFormFields,
} from './SupportTicketDialog';

export interface AccountLimitCustomFields extends CustomFields {
linodePlan: string;
numberOfEntities: string;
}

export const SupportTicketAccountLimitFields = () => {
interface Props {
prefilledFormPayloadValues?: FormPayloadValues;
}

export const SupportTicketAccountLimitFields = ({
prefilledFormPayloadValues,
}: Props) => {
const { control, formState, reset, watch } = useFormContext<
AccountLimitCustomFields & SupportTicketFormFields
>();
const { entityType } = watch();

const { data: account } = useAccount();
const prefilledLinodeType =
prefilledFormPayloadValues && 'type' in prefilledFormPayloadValues
? prefilledFormPayloadValues.type
: '';
const prefilledKubeTypes =
prefilledFormPayloadValues && 'node_pools' in prefilledFormPayloadValues
? prefilledFormPayloadValues.node_pools?.map((pool) => pool.type)
: [];

const { entityType } = watch();
const { data: account } = useAccount();
const { data: linodeType } = useTypeQuery(
prefilledLinodeType ?? '',
Boolean(prefilledLinodeType)
);
const kubeTypesQuery = useSpecificTypes(
prefilledKubeTypes ?? [],
Boolean(prefilledKubeTypes)
);
const kubeTypes = extendTypesQueryResult(kubeTypesQuery);

// Must be in the same order as the fields are displayed in the form.
const defaultValues = {
companyName: account?.company,
customerName: `${account?.first_name} ${account?.last_name}`,
...formState.defaultValues,
customerName: `${account?.first_name ?? ''} ${account?.last_name ?? ''}`,
// eslint-disable-next-line perfectionist/sort-objects
companyName: account?.company ?? '',
numberOfEntities: '',
// eslint-disable-next-line perfectionist/sort-objects
linodePlan:
linodeType?.label ??
kubeTypes.map((type) => type.formattedLabel).join(', ') ??
'',
useCase: '',
// eslint-disable-next-line perfectionist/sort-objects
publicInfo: '',
};

const shouldShowLinodePlanField =
Expand Down Expand Up @@ -95,10 +135,13 @@ export const SupportTicketAccountLimitFields = () => {
<Controller
render={({ field, fieldState }) => (
<TextField
label={ACCOUNT_LIMIT_FIELD_NAME_TO_LABEL_MAP.useCase.replace(
'entities',
getEntityNameFromEntityType(entityType, true)
)}
data-qa-ticket-use-case
errorText={fieldState.error?.message}
expand
label={ACCOUNT_LIMIT_FIELD_NAME_TO_LABEL_MAP.useCase}
multiline
name="useCase"
onChange={field.onChange}
Expand Down
Loading

0 comments on commit 81082b0

Please sign in to comment.