diff --git a/packages/manager/.changeset/pr-10745-changed-1723666784731.md b/packages/manager/.changeset/pr-10745-changed-1723666784731.md
new file mode 100644
index 00000000000..1db2047bf20
--- /dev/null
+++ b/packages/manager/.changeset/pr-10745-changed-1723666784731.md
@@ -0,0 +1,5 @@
+---
+"@linode/manager": Changed
+---
+
+Improve support ticket form pre-population and field labels ([#10745](https://github.com/linode/manager/pull/10745))
diff --git a/packages/manager/cypress/e2e/core/helpAndSupport/open-support-ticket.spec.ts b/packages/manager/cypress/e2e/core/helpAndSupport/open-support-ticket.spec.ts
index 07636b6f0ee..bffc53d6cef 100644
--- a/packages/manager/cypress/e2e/core/helpAndSupport/open-support-ticket.spec.ts
+++ b/packages/manager/cypress/e2e/core/helpAndSupport/open-support-ticket.spec.ts
@@ -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(),
};
@@ -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')
@@ -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()
@@ -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');
}
);
});
diff --git a/packages/manager/src/components/ErrorMessage.tsx b/packages/manager/src/components/ErrorMessage.tsx
index b0092809348..68917fb0fd3 100644
--- a/packages/manager/src/components/ErrorMessage.tsx
+++ b/packages/manager/src/components/ErrorMessage.tsx
@@ -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 (
);
diff --git a/packages/manager/src/components/SupportLink/SupportLink.tsx b/packages/manager/src/components/SupportLink/SupportLink.tsx
index 16dd526b831..a9a112354e6 100644
--- a/packages/manager/src/components/SupportLink/SupportLink.tsx
+++ b/packages/manager/src/components/SupportLink/SupportLink.tsx
@@ -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;
@@ -22,7 +24,16 @@ 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 (
{
state: {
description,
entity,
+ formPayloadValues,
open: true,
ticketType,
title,
diff --git a/packages/manager/src/components/SupportTicketGeneralError.tsx b/packages/manager/src/components/SupportTicketGeneralError.tsx
index 6e09556dc37..d026c879ef9 100644
--- a/packages/manager/src/components/SupportTicketGeneralError.tsx
+++ b/packages/manager/src/components/SupportTicketGeneralError.tsx
@@ -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;
}
@@ -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);
@@ -50,6 +54,7 @@ export const SupportTicketGeneralError = (
isAccountLimitSupportTicket ? 'accountLimit' : 'general'
}
entity={{ id: undefined, type: entityType }}
+ formPayloadValues={formPayloadValues}
key={`${substring}-${idx}`}
/>
);
diff --git a/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx b/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx
index 2ff221c7f2c..2e82e2817e7 100644
--- a/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx
+++ b/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx
@@ -212,7 +212,11 @@ export const CreateCluster = () => {
{generalError && (
-
+
)}
diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Error.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/Error.tsx
index 833742288a5..0b8dc8ed2ff 100644
--- a/packages/manager/src/features/Linodes/LinodeCreatev2/Error.tsx
+++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Error.tsx
@@ -10,9 +10,11 @@ import type { CreateLinodeRequest } from '@linode/api-v4';
export const Error = () => {
const {
formState: { errors },
+ getValues,
} = useFormContext();
const generalError = errors.root?.message ?? errors.interfaces?.message;
+ const values = getValues();
if (!generalError) {
return null;
@@ -21,7 +23,11 @@ export const Error = () => {
return (
-
+
);
diff --git a/packages/manager/src/features/Linodes/LinodesCreate/LinodeCreate.tsx b/packages/manager/src/features/Linodes/LinodesCreate/LinodeCreate.tsx
index 80255fcf94d..d214eee5753 100644
--- a/packages/manager/src/features/Linodes/LinodesCreate/LinodeCreate.tsx
+++ b/packages/manager/src/features/Linodes/LinodesCreate/LinodeCreate.tsx
@@ -846,7 +846,11 @@ export class LinodeCreate extends React.PureComponent<
)}
{generalError && (
-
+
)}
{userCannotCreateLinode && (
diff --git a/packages/manager/src/features/Support/SupportTickets/SupportTicketAccountLimitFields.tsx b/packages/manager/src/features/Support/SupportTickets/SupportTicketAccountLimitFields.tsx
index 3c1f061adaa..271ff3dfbcc 100644
--- a/packages/manager/src/features/Support/SupportTickets/SupportTicketAccountLimitFields.tsx
+++ b/packages/manager/src/features/Support/SupportTickets/SupportTicketAccountLimitFields.tsx
@@ -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 =
@@ -95,10 +135,13 @@ export const SupportTicketAccountLimitFields = () => {
(
+ | Partial;
+
export interface TicketTypeData {
dialogTitle: string;
helperText: JSX.Element | string;
@@ -146,33 +153,48 @@ export const SupportTicketDialog = (props: SupportTicketDialogProps) => {
prefilledTitle,
} = props;
+ const location = useLocation();
+ const stateParams = location.state;
+
+ // Collect prefilled data from props or Link parameters.
+ const _prefilledDescription: string =
+ prefilledDescription ?? stateParams?.description ?? undefined;
+ const _prefilledEntity: EntityForTicketDetails =
+ prefilledEntity ?? stateParams?.entity ?? undefined;
+ const _prefilledTitle: string =
+ prefilledTitle ?? stateParams?.title ?? undefined;
+ const prefilledFormPayloadValues: FormPayloadValues =
+ stateParams?.formPayloadValues ?? undefined;
+ const _prefilledTicketType: TicketType =
+ prefilledTicketType ?? stateParams?.ticketType ?? undefined;
+
+ // Use the prefilled title if one is given, otherwise, use any default prefill titles by ticket type, if extant.
+ const newPrefilledTitle = _prefilledTitle
+ ? _prefilledTitle
+ : _prefilledTicketType && TICKET_TYPE_MAP[_prefilledTicketType]
+ ? TICKET_TYPE_MAP[_prefilledTicketType].ticketTitle
+ : undefined;
+
const formContainerRef = React.useRef(null);
const hasSeverityCapability = useTicketSeverityCapability();
const valuesFromStorage = storage.supportTicket.get();
- // Use a prefilled title if one is given, otherwise, use any default prefill titles by ticket type, if extant.
- const _prefilledTitle = prefilledTitle
- ? prefilledTitle
- : prefilledTicketType && TICKET_TYPE_MAP[prefilledTicketType]
- ? TICKET_TYPE_MAP[prefilledTicketType].ticketTitle
- : undefined;
-
// Ticket information
const form = useForm({
defaultValues: {
description: getInitialValue(
- prefilledDescription,
+ _prefilledDescription,
valuesFromStorage.description
),
- entityId: prefilledEntity?.id ? String(prefilledEntity.id) : '',
+ entityId: _prefilledEntity?.id ? String(_prefilledEntity.id) : '',
entityInputValue: '',
- entityType: prefilledEntity?.type ?? 'general',
- summary: getInitialValue(_prefilledTitle, valuesFromStorage.summary),
- ticketType: prefilledTicketType ?? 'general',
+ entityType: _prefilledEntity?.type ?? 'general',
+ summary: getInitialValue(newPrefilledTitle, valuesFromStorage.summary),
+ ticketType: _prefilledTicketType ?? 'general',
},
- resolver: yupResolver(SCHEMA_MAP[prefilledTicketType ?? 'general']),
+ resolver: yupResolver(SCHEMA_MAP[_prefilledTicketType ?? 'general']),
});
const {
@@ -458,7 +480,11 @@ export const SupportTicketDialog = (props: SupportTicketDialogProps) => {
>
)}
{ticketType === 'smtp' && }
- {ticketType === 'accountLimit' && }
+ {ticketType === 'accountLimit' && (
+
+ )}
{(!ticketType || ticketType === 'general') && (
<>
{props.hideProductSelection ? null : (
diff --git a/packages/manager/src/features/Support/SupportTickets/SupportTicketProductSelectionFields.tsx b/packages/manager/src/features/Support/SupportTickets/SupportTicketProductSelectionFields.tsx
index 57deb302df3..1998e626fc5 100644
--- a/packages/manager/src/features/Support/SupportTickets/SupportTicketProductSelectionFields.tsx
+++ b/packages/manager/src/features/Support/SupportTickets/SupportTicketProductSelectionFields.tsx
@@ -18,6 +18,7 @@ import {
ENTITY_ID_TO_NAME_MAP,
ENTITY_MAP,
} from './constants';
+import { getEntityNameFromEntityType } from './ticketUtils';
import type { AccountLimitCustomFields } from './SupportTicketAccountLimitFields';
import type {
@@ -183,10 +184,7 @@ export const SupportTicketProductSelectionFields = (props: Props) => {
return eachTopic.value === entityType;
});
- const _entityType =
- entityType !== 'general' && entityType !== 'none'
- ? `${ENTITY_ID_TO_NAME_MAP[entityType]}s`
- : 'entities';
+ const _entityType = getEntityNameFromEntityType(entityType, true);
return (
// eslint-disable-next-line react/jsx-no-useless-fragment
@@ -195,10 +193,13 @@ export const SupportTicketProductSelectionFields = (props: Props) => {
(
{
stateParams ? stateParams.open : parsedParams.drawerOpen === 'true'
);
- // @todo this should be handled in the support ticket component
- // and probably does not need to use state
- const [prefilledDescription] = React.useState(
- stateParams ? stateParams.description : undefined
- );
- const [prefilledTitle] = React.useState(
- stateParams ? stateParams.title : undefined
- );
- const [prefilledEntity] = React.useState(
- stateParams ? stateParams.entity : undefined
- );
- const [prefilledTicketType] = React.useState(
- stateParams ? stateParams.ticketType : undefined
- );
-
const handleAddTicketSuccess = (
ticketId: number,
attachmentErrors: AttachmentError[] = []
@@ -106,10 +91,6 @@ const SupportTicketsLanding = () => {
onClose={() => setDrawerOpen(false)}
onSuccess={handleAddTicketSuccess}
open={drawerOpen}
- prefilledDescription={prefilledDescription}
- prefilledEntity={prefilledEntity}
- prefilledTicketType={prefilledTicketType}
- prefilledTitle={prefilledTitle}
/>
);
diff --git a/packages/manager/src/features/Support/SupportTickets/constants.tsx b/packages/manager/src/features/Support/SupportTickets/constants.tsx
index 3bb669ca145..9e1134a0f02 100644
--- a/packages/manager/src/features/Support/SupportTickets/constants.tsx
+++ b/packages/manager/src/features/Support/SupportTickets/constants.tsx
@@ -88,8 +88,9 @@ export const ENTITY_ID_TO_NAME_MAP: Record = {
// General custom fields common to multiple custom ticket types.
export const CUSTOM_FIELD_NAME_TO_LABEL_MAP: Record = {
- companyName: 'Business or company name',
customerName: 'First and last name',
+ // eslint-disable-next-line perfectionist/sort-objects
+ companyName: 'Business or company name',
publicInfo:
"Links to public information - e.g. your business or application's website, Twitter profile, GitHub, etc.",
useCase: 'A clear and detailed description of your use case',
@@ -104,8 +105,9 @@ export const SMTP_FIELD_NAME_TO_LABEL_MAP: Record = {
export const ACCOUNT_LIMIT_FIELD_NAME_TO_LABEL_MAP: Record = {
...CUSTOM_FIELD_NAME_TO_LABEL_MAP,
- linodePlan: 'Which Linode plan do you need access to?',
numberOfEntities: 'Total number of entities you need?',
+ // eslint-disable-next-line perfectionist/sort-objects
+ linodePlan: 'Which Linode plan do you need access to?',
useCase:
'A detailed description of your use case and why you need access to more/larger entities',
};
diff --git a/packages/manager/src/features/Support/SupportTickets/ticketUtils.test.ts b/packages/manager/src/features/Support/SupportTickets/ticketUtils.test.ts
index 025f0469bea..e30ec784902 100644
--- a/packages/manager/src/features/Support/SupportTickets/ticketUtils.test.ts
+++ b/packages/manager/src/features/Support/SupportTickets/ticketUtils.test.ts
@@ -1,5 +1,5 @@
import { SMTP_FIELD_NAME_TO_LABEL_MAP } from './constants';
-import { formatDescription } from './ticketUtils';
+import { formatDescription, getEntityNameFromEntityType } from './ticketUtils';
import type { SupportTicketFormFields } from './SupportTicketDialog';
import type { SMTPCustomFields } from './SupportTicketSMTPFields';
@@ -47,3 +47,19 @@ describe('formatDescription', () => {
).toEqual(expectedFormattedDescription);
});
});
+
+describe('getEntityNameFromEntityType', () => {
+ it('returns a human-readable entity name or default from the entity type', () => {
+ const nbEntityType = 'nodebalancer_id';
+ const generalEntityType = 'general';
+
+ expect(getEntityNameFromEntityType(nbEntityType)).toEqual('NodeBalancer');
+ expect(getEntityNameFromEntityType(nbEntityType, true)).toEqual(
+ 'NodeBalancers'
+ );
+ expect(getEntityNameFromEntityType(generalEntityType)).toEqual('entity');
+ expect(getEntityNameFromEntityType(generalEntityType, true)).toEqual(
+ 'entities'
+ );
+ });
+});
diff --git a/packages/manager/src/features/Support/SupportTickets/ticketUtils.ts b/packages/manager/src/features/Support/SupportTickets/ticketUtils.ts
index 2aa2d2a7212..c3bc84d61a7 100644
--- a/packages/manager/src/features/Support/SupportTickets/ticketUtils.ts
+++ b/packages/manager/src/features/Support/SupportTickets/ticketUtils.ts
@@ -6,12 +6,14 @@ import { isFeatureEnabled } from 'src/utilities/accountCapabilities';
import {
ACCOUNT_LIMIT_FIELD_NAME_TO_LABEL_MAP,
+ ENTITY_ID_TO_NAME_MAP,
SMTP_FIELD_NAME_TO_LABEL_MAP,
TICKET_TYPE_TO_CUSTOM_FIELD_KEYS_MAP,
} from './constants';
import type {
AllSupportTicketFormFields,
+ EntityType,
SupportTicketFormFields,
TicketType,
} from './SupportTicketDialog';
@@ -105,9 +107,33 @@ export const formatDescription = (
if (ticketType === 'smtp') {
label = SMTP_FIELD_NAME_TO_LABEL_MAP[key];
} else if (ticketType === 'accountLimit') {
- label = ACCOUNT_LIMIT_FIELD_NAME_TO_LABEL_MAP[key];
+ // Use the specific entity type in the form labels to match what is displayed to the user.
+ if (key === 'numberOfEntities' || key === 'useCase') {
+ label = ACCOUNT_LIMIT_FIELD_NAME_TO_LABEL_MAP[key].replace(
+ 'entities',
+ getEntityNameFromEntityType(values.entityType, true)
+ );
+ } else {
+ label = ACCOUNT_LIMIT_FIELD_NAME_TO_LABEL_MAP[key];
+ }
}
return `**${label}**\n${value ? value : 'No response'}`;
})
.join('\n\n');
};
+
+/**
+ * getEntityNameFromEntityType
+ *
+ * @param entityType - the entity type submitted with the support ticket; ends in '_id'
+ * @param isPlural - if true, pluralize the entity name; defaults to false
+ * @returns human readable entity name, singular or plural; falls back on generic 'entity'
+ */
+export const getEntityNameFromEntityType = (
+ entityType: EntityType,
+ isPlural = false
+) => {
+ return entityType !== 'general' && entityType !== 'none'
+ ? `${ENTITY_ID_TO_NAME_MAP[entityType]}${isPlural ? 's' : ''}`
+ : `${isPlural ? 'entities' : 'entity'}`;
+};