Skip to content
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

M3-5988: Add SMTP restriction support ticket form #8636

Merged
Merged
9 changes: 7 additions & 2 deletions packages/manager/src/components/SupportLink/SupportLink.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import * as React from 'react';
import { Link, LinkProps } from 'react-router-dom';
import { EntityType } from 'src/features/Support/SupportTickets/SupportTicketDrawer';
import {
EntityType,
TicketType,
} from 'src/features/Support/SupportTickets/SupportTicketDrawer';

interface Props {
title?: string;
description?: string;
text: string;
entity?: EntityForTicketDetails;
ticketType?: TicketType;
onClick?: LinkProps['onClick'];
}

Expand All @@ -16,7 +20,7 @@ export interface EntityForTicketDetails {
}

const SupportLink = (props: Props) => {
const { description, text, title, entity, onClick } = props;
const { description, text, title, entity, ticketType, onClick } = props;
return (
<Link
to={{
Expand All @@ -26,6 +30,7 @@ const SupportLink = (props: Props) => {
title,
description,
entity,
ticketType,
},
}}
onClick={onClick}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import SectionErrorBoundary from 'src/components/SectionErrorBoundary';
import { EntityForTicketDetails } from 'src/components/SupportLink/SupportLink';
import TextField from 'src/components/TextField';
import useEntities, { Entity } from 'src/hooks/useEntities';
import { useAccount } from 'src/queries/account';
import { useAllDatabasesQuery } from 'src/queries/databases';
import { useAllDomainsQuery } from 'src/queries/domains';
import { useAllFirewallsQuery } from 'src/queries/firewalls';
Expand All @@ -37,6 +38,11 @@ import { FileAttachment } from '../index';
import { AttachmentError } from '../SupportTicketDetail/SupportTicketDetail';
import Reference from '../SupportTicketDetail/TabbedReply/MarkdownReference';
import TabbedReply from '../SupportTicketDetail/TabbedReply/TabbedReply';
import SupportTicketSMTPFields, {
smtpDialogTitle,
fieldNameToLabelMap,
smtpHelperText,
} from './SupportTicketSMTPFields';

const useStyles = makeStyles((theme: Theme) => ({
expPanelSummary: {
Expand Down Expand Up @@ -79,11 +85,19 @@ export type EntityType =
| 'none'
| 'general';

export type TicketType = 'smtp' | 'general';

interface TicketTypeData {
dialogTitle: string;
helperText: string | JSX.Element;
}

export interface Props {
open: boolean;
prefilledTitle?: string;
prefilledDescription?: string;
prefilledEntity?: EntityForTicketDetails;
prefilledTicketType?: TicketType;
onClose: () => void;
onSuccess: (ticketId: number, attachmentErrors?: AttachmentError[]) => void;
keepOpenOnSuccess?: boolean;
Expand All @@ -92,6 +106,32 @@ export interface Props {

export type CombinedProps = Props;

const ticketTypeMap: Record<TicketType, TicketTypeData> = {
smtp: {
dialogTitle: smtpDialogTitle,
helperText: smtpHelperText,
},
general: {
dialogTitle: 'Open a Support Ticket',
helperText: (
<>
{`We love our customers, and we\u{2019}re here to help if you need us.
Please keep in mind that not all topics are within the scope of our support.
For overall system status, please see `}
<a
href="https://status.linode.com"
target="_blank"
aria-describedby="external-site"
rel="noopener noreferrer"
>
status.linode.com
</a>
.
</>
),
},
};

const entityMap: Record<string, EntityType> = {
Linodes: 'linode_id',
Volumes: 'volume_id',
Expand Down Expand Up @@ -141,7 +181,15 @@ export const getInitialValue = (
};

export const SupportTicketDrawer: React.FC<CombinedProps> = (props) => {
const { open, prefilledDescription, prefilledTitle, prefilledEntity } = props;
const {
open,
prefilledDescription,
prefilledTitle,
prefilledEntity,
prefilledTicketType,
} = props;

const { data: account } = useAccount();

const valuesFromStorage = storage.supportText.get();

Expand All @@ -158,6 +206,18 @@ export const SupportTicketDrawer: React.FC<CombinedProps> = (props) => {
const [entityID, setEntityID] = React.useState<string>(
prefilledEntity ? String(prefilledEntity.id) : ''
);
const [ticketType, setTicketType] = React.useState<TicketType>(
prefilledTicketType ?? 'general'
);

// SMTP ticket information
const [smtpFields, setSMTPFields] = React.useState({
customerName: account ? `${account?.first_name} ${account?.last_name}` : '',
companyName: '',
useCase: '',
emailDomains: '',
publicInfo: '',
});

// Entities for populating dropdown
const [data, setData] = React.useState<Item<any>[]>([]);
Expand All @@ -181,6 +241,10 @@ export const SupportTicketDrawer: React.FC<CombinedProps> = (props) => {
}
}, [open]);

React.useEffect(() => {
setDescription(formatDescription(smtpFields));
}, [smtpFields]);

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 might be the culprit for why smtp fields are displaying in the desc for normal support tickets. If I comment this out and clear my application storage, the desc is no longer filled.

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe only format the description on submission and not on every change would be a fix?

Copy link
Contributor Author

@mjac0bs mjac0bs Dec 10, 2022

Choose a reason for hiding this comment

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

Okay, so there is different behavior for the dialog based on how the user cancels it, as documented in this PR:

  • If the user hits the Cancel button, the SMTP desc is not visible in the desc of normal support tickets. (Expected behavior).
  • If the user hits the 'X', ESC, or clicks outside of the dialog, the contents of the form are saved in local storage rather than cleared. Because the close method didn't know anything about the new SMTP ticketType, it was saving that description to local storage and then repopulating it for a normal ticket's description.

Agreed, it would be best to only format the custom fields -> description onSubmit rather than on each update to the SMTP fields… but since onSubmit's call to createSupportTicket needs a description param, we're expecting to use description from state. If we format and attempt a final state update with setDescription on submission, the update may not happen before the ticket is created. Always open to suggestions or ideas I've missed, but the latest commit conditionally checking ticketType in the close function should address this bug by clearing description for SMTP tickets.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Disregard the last bit of above ^^ Formatting of the description now only occurs on submission, with an adjustment to the logic that toggles the boolean controlling the submit button from disabled to enabled.

I left the conditional checking ticketType in close() in order to always clear the title of the SMTP ticket when the form is closed out. Note: there is still an edge case when a form title can repopulate in a new support ticket's title field if the user was first redirected to support/tickets with any support ticket having a prefilled title. This behavior exists in prod and predates the SMTP form addition.

// React Query entities
const { data: databases, isLoading: databasesLoading } = useAllDatabasesQuery(
entityType === 'database_id'
Expand Down Expand Up @@ -283,6 +347,7 @@ export const SupportTicketDrawer: React.FC<CombinedProps> = (props) => {
setDescription(_description);
setEntityID('');
setEntityType('general');
setTicketType('general');
};

const resetDrawer = (clearValues: boolean = false) => {
Expand Down Expand Up @@ -321,6 +386,24 @@ export const SupportTicketDrawer: React.FC<CombinedProps> = (props) => {
setEntityID(String(selected?.value) ?? '');
};

const handleSMTPFieldChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setSMTPFields((smtpFields) => ({ ...smtpFields, [name]: value }));
};

/**
* When variant ticketTypes include additional fields, fields must concat to one description string.
* For readability, replace field names with field labels and format the description in Markdown.
*/
const formatDescription = (fields: Record<string, string>) => {
return Object.entries(fields)
.map(
([key, value]) =>
`**${fieldNameToLabelMap[key]}**\n${value ? value : 'No response'}`
)
.join('\n\n');
};

const close = () => {
props.onClose();
};
Expand Down Expand Up @@ -447,7 +530,15 @@ export const SupportTicketDrawer: React.FC<CombinedProps> = (props) => {
});
};

const requirementsMet = description.length > 0 && summary.length > 0;
const smtpRequirementsMet =
smtpFields.customerName.length > 0 &&
smtpFields.useCase.length > 0 &&
smtpFields.emailDomains.length > 0 &&
smtpFields.publicInfo.length > 0;
const requirementsMet =
description.length > 0 &&
summary.length > 0 &&
(ticketType === 'smtp' ? smtpRequirementsMet : ticketType === 'general');

const hasErrorFor = getErrorMap(['summary', 'description', 'input'], errors);
const summaryError = hasErrorFor.summary;
Expand Down Expand Up @@ -530,25 +621,14 @@ export const SupportTicketDrawer: React.FC<CombinedProps> = (props) => {
onClose={close}
fullHeight
fullWidth
title="Open a Support Ticket"
title={ticketTypeMap[ticketType].dialogTitle}
>
{props.children || (
<React.Fragment>
{generalError && <Notice error text={generalError} data-qa-notice />}

<Typography data-qa-support-ticket-helper-text>
{`We love our customers, and we\u{2019}re here to help if you need us.
Please keep in mind that not all topics are within the scope of our support.
For overall system status, please see `}
<a
href="https://status.linode.com"
target="_blank"
aria-describedby="external-site"
rel="noopener noreferrer"
>
status.linode.com
</a>
.
{ticketTypeMap[ticketType].helperText}
</Typography>
<TextField
label="Title"
Expand All @@ -560,58 +640,67 @@ export const SupportTicketDrawer: React.FC<CombinedProps> = (props) => {
errorText={summaryError}
data-qa-ticket-summary
/>
{props.hideProductSelection ? null : (
{ticketType === 'smtp' ? (
<SupportTicketSMTPFields
handleChange={handleSMTPFieldChange}
formState={smtpFields}
/>
) : (
<React.Fragment>
<Select
options={topicOptions}
label="What is this regarding?"
value={selectedTopic}
onChange={handleEntityTypeChange}
data-qa-ticket-entity-type
isClearable={false}
/>
{!['none', 'general'].includes(entityType) && (
<>
{props.hideProductSelection ? null : (
<React.Fragment>
<Select
options={entityOptions}
value={selectedEntity}
disabled={entityOptions.length === 0}
errorText={entityError || inputError}
placeholder={`Select a ${entityIdToNameMap[entityType]}`}
label={entityIdToNameMap[entityType] ?? 'Entity Select'}
onChange={handleEntityIDChange}
data-qa-ticket-entity-id
isLoading={areEntitiesLoading}
options={topicOptions}
label="What is this regarding?"
value={selectedTopic}
onChange={handleEntityTypeChange}
data-qa-ticket-entity-type
isClearable={false}
/>
{!areEntitiesLoading && entityOptions.length === 0 ? (
<FormHelperText>
You don&rsquo;t have any {entityIdToNameMap[entityType]}s
on your account.
</FormHelperText>
) : null}
</>
{!['none', 'general'].includes(entityType) && (
<>
<Select
options={entityOptions}
value={selectedEntity}
disabled={entityOptions.length === 0}
errorText={entityError || inputError}
placeholder={`Select a ${entityIdToNameMap[entityType]}`}
label={entityIdToNameMap[entityType] ?? 'Entity Select'}
onChange={handleEntityIDChange}
data-qa-ticket-entity-id
isLoading={areEntitiesLoading}
isClearable={false}
/>
{!areEntitiesLoading && entityOptions.length === 0 ? (
<FormHelperText>
You don&rsquo;t have any{' '}
{entityIdToNameMap[entityType]}s on your account.
</FormHelperText>
) : null}
</>
)}
</React.Fragment>
)}
<TabbedReply
required
error={descriptionError}
handleChange={handleDescriptionInputChange}
value={description}
innerClass={classes.innerReply}
rootClass={classes.rootReply}
placeholder={
"Tell us more about the trouble you're having and any steps you've already taken to resolve it."
}
/>
<Accordion
heading="Formatting Tips"
detailProps={{ className: classes.expPanelSummary }}
>
<Reference />
</Accordion>
<AttachFileForm files={files} updateFiles={updateFiles} />
</React.Fragment>
)}
<TabbedReply
required
error={descriptionError}
handleChange={handleDescriptionInputChange}
value={description}
innerClass={classes.innerReply}
rootClass={classes.rootReply}
placeholder={
"Tell us more about the trouble you're having and any steps you've already taken to resolve it."
}
/>
<Accordion
heading="Formatting Tips"
detailProps={{ className: classes.expPanelSummary }}
>
<Reference />
</Accordion>
<AttachFileForm files={files} updateFiles={updateFiles} />
<ActionsPanel>
<Button
buttonType="secondary"
Expand Down
Loading