Skip to content

update support ticket to support categories, project, and target options #6689

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

Open
wants to merge 28 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
3be1fcf
update support ticket to support categories, project, and target options
Apr 1, 2025
5bcf8e6
Merge branch 'main' into support-ticket-updates
egoodwinx Apr 2, 2025
a34b349
Merge branch 'graphql-hive:main' into support-ticket-updates
egoodwinx Apr 3, 2025
23c28a4
address comments
Apr 3, 2025
a40fdbf
Merge branch 'main' into support-ticket-updates
egoodwinx Apr 4, 2025
4feb3ff
Merge branch 'graphql-hive:main' into support-ticket-updates
egoodwinx Apr 6, 2025
c0cce7b
added resolver logic
Apr 6, 2025
24677e8
Merge branch 'main' into support-ticket-updates
jdolle Apr 14, 2025
c88dd7b
Merge branch 'graphql-hive:main' into support-ticket-updates
egoodwinx Apr 15, 2025
edf992c
Merge branch 'graphql-hive:main' into support-ticket-updates
egoodwinx Apr 16, 2025
35326cf
Merge branch 'graphql-hive:main' into support-ticket-updates
egoodwinx May 1, 2025
e41ece0
Merge branch 'graphql-hive:main' into support-ticket-updates
egoodwinx May 6, 2025
5f0d6ac
update ticket message and optional category metadata
Apr 16, 2025
d6217f5
remove category, project, target from schema since its in the body, u…
May 6, 2025
fbf0c63
Merge branch 'main' into support-ticket-updates
jdolle May 7, 2025
9c8bf4a
update to fix lint errors
May 15, 2025
e76a0b1
Merge branch 'main' into support-ticket-updates
jdolle May 15, 2025
9a162e5
fix SupportCategoryType typecheck errors
May 19, 2025
f5f1438
Merge branch 'main' into support-ticket-updates
egoodwinx May 19, 2025
a08864a
Merge branch 'main' into support-ticket-updates
egoodwinx Jun 1, 2025
1bd9716
Merge branch 'main' into support-ticket-updates
jdolle Jun 2, 2025
d412ed0
Merge branch 'main' into support-ticket-updates
egoodwinx Jun 8, 2025
24bf245
pnpm prettier and lint fixes
Jun 23, 2025
a2e5a12
fix merge issues
Jun 23, 2025
fc84dea
Merge branch 'main' into support-ticket-updates
jdolle Jun 23, 2025
f979a00
fix typecheck
Jun 24, 2025
539a7d2
fix lint issue
Jun 24, 2025
63b812a
fix prettier
Jun 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions codegen.mts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const config: CodegenConfig = {
OrganizationAccessScope: '../modules/auth/providers/scopes#OrganizationAccessScope',
SupportTicketPriority: '../shared/entities#SupportTicketPriority',
SupportTicketStatus: '../shared/entities#SupportTicketStatus',
SupportCategoryType: '../shared/entities#SupportCategoryType',
},
resolversNonOptionalTypename: {
interfaceImplementingType: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,9 @@ export const AuditLogModel = z.union([
metadata: z.object({
ticketId: z.string(),
ticketSubject: z.string(),
ticketCategory: z.string().optional(),
ticketProject: z.string().optional(),
ticketTarget: z.string().optional(),
ticketDescription: z.string(),
ticketPriority: z.string(),
}),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { SupportTicketPriority, SupportTicketStatus } from '../../shared/entities';
import {
SupportCategoryType,
SupportTicketPriority,
SupportTicketStatus,
} from '../../shared/entities';

export type SupportTicketPriorityMapper = SupportTicketPriority;
export type SupportTicketStatusMapper = SupportTicketStatus;
export type SupportCategoryTypeMapper = SupportCategoryType;
10 changes: 10 additions & 0 deletions packages/services/api/src/modules/support/module.graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ export default gql`

input SupportTicketCreateInput {
organizationSlug: String!
projectId: String
targetId: String
category: SupportCategoryType
subject: String!
description: String!
priority: SupportTicketPriority!
Expand Down Expand Up @@ -95,6 +98,13 @@ export default gql`
fromSupport: Boolean!
}

enum SupportCategoryType {
TECHNICAL_ISSUE
BILLING
COMPLIANCE
OTHER
}

enum SupportTicketPriority {
NORMAL
HIGH
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { createHash } from 'node:crypto';
import { Inject, Injectable, Scope } from 'graphql-modules';
import { z } from 'zod';
import { Organization, SupportTicketPriority, SupportTicketStatus } from '../../../shared/entities';
import {
Organization,
SupportCategoryType,
SupportTicketPriority,
SupportTicketStatus,
} from '../../../shared/entities';
import { atomic } from '../../../shared/helpers';
import { AuditLogRecorder } from '../../audit-logs/providers/audit-log-recorder';
import { Session } from '../../auth/lib/authz';
Expand All @@ -23,6 +28,7 @@ export const SupportTicketStatusAPIModel = z.enum([

export const SupportTicketPriorityModel = z.nativeEnum(SupportTicketPriority);
export const SupportTicketStatusModel = z.nativeEnum(SupportTicketStatus);
export const SupportTicketCategoryModel = z.nativeEnum(SupportCategoryType);

const SupportTicketModel = z.object({
id: z.number(),
Expand All @@ -45,6 +51,9 @@ const SupportTicketModel = z.object({

return SupportTicketStatusModel.parse(value);
}),
category: SupportTicketCategoryModel,
project: z.string().optional(),
target: z.string().optional(),
created_at: z.string(),
updated_at: z.string(),
subject: z.string(),
Expand Down Expand Up @@ -80,6 +89,9 @@ const SupportTicketCommentListModel = z.object({

const SupportTicketCreateRequestModel = z.object({
organizationId: z.string(),
category: SupportTicketCategoryModel,
project: z.string().optional(),
target: z.string().optional(),
subject: z.string().min(3),
description: z.string().min(3),
priority: SupportTicketPriorityModel,
Expand Down Expand Up @@ -508,6 +520,9 @@ export class SupportManager {
organizationId: string;
subject: string;
description: string;
category?: z.infer<typeof SupportTicketCategoryModel>;
project?: string;
target?: string;
priority: z.infer<typeof SupportTicketPriorityModel>;
}) {
this.logger.info(
Expand Down Expand Up @@ -549,6 +564,9 @@ export class SupportManager {
subject: input.subject,
description: input.description,
priority: input.priority,
category: input.category,
project: input.project,
target: input.target,
// version is here to cache bust the idempotency key.
version: 'v2',
}),
Expand All @@ -560,6 +578,12 @@ export class SupportManager {
});
const customerType = this.resolveCustomerType(organization);

const formattedBody = ` "Category: " + ${request.data.category ? request.data.category : 'Not Selected'}\n\n
"Project: " + ${request.data.project ? request.data.project : 'Not Selected'}\n\n
"Target: " + ${request.data.target ? request.data.target : 'Not Selected'}\n\n
"Description: " + ${request.data.description}
`;

const response = await this.httpClient
.post(`https://${this.config.subdomain}.zendesk.com/api/v2/tickets`, {
username: this.config.username,
Expand All @@ -570,7 +594,7 @@ export class SupportManager {
submitter_id: parseInt(internalUserId, 10),
requester_id: parseInt(internalUserId, 10),
comment: {
body: request.data.description,
body: formattedBody,
},
priority: request.data.priority,
subject: request.data.subject,
Expand Down Expand Up @@ -598,6 +622,9 @@ export class SupportManager {
metadata: {
ticketDescription: input.description,
ticketPriority: input.priority,
...(input.category ? { ticketCategory: input.category } : {}),
...(input.project ? { ticketProject: input.project } : {}),
...(input.target ? { ticketTarget: input.target } : {}),
ticketId: String(response.ticket.id),
ticketSubject: input.subject,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const supportTicketCreate: NonNullable<MutationResolvers['supportTicketCr
const response = await injector.get(SupportManager).createTicket({
organizationId,
...input,
category: input.category ?? undefined,
});

return response;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { SupportCategoryType as SupportCategoryTypeEnum } from '../../../shared/entities';
import type { SupportCategoryTypeResolvers } from './../../../__generated__/types';

export const SupportCategoryType: SupportCategoryTypeResolvers = {
TECHNICAL_ISSUE: SupportCategoryTypeEnum.TECHNICAL_ISSUE,
BILLING: SupportCategoryTypeEnum.BILLING,
COMPLIANCE: SupportCategoryTypeEnum.COMPLIANCE,
OTHER: SupportCategoryTypeEnum.OTHER,
};
7 changes: 7 additions & 0 deletions packages/services/api/src/shared/entities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,13 @@ export enum SupportTicketPriority {
URGENT = 'urgent',
}

export enum SupportCategoryType {
BILLING = 'billing',
COMPLIANCE = 'compliance',
OTHER = 'other',
TECHNICAL_ISSUE = 'technical_issue',
}

export enum SupportTicketStatus {
OPEN = 'open',
SOLVED = 'solved',
Expand Down
87 changes: 54 additions & 33 deletions packages/web/app/src/components/layouts/project-selector.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Select, SelectContent, SelectItem, SelectTrigger } from '@/components/ui/select';
import { FragmentType, graphql, useFragment } from '@/gql';
import { SelectValue } from '@radix-ui/react-select';
import { Link, useRouter } from '@tanstack/react-router';

const ProjectSelector_OrganizationConnectionFragment = graphql(`
Expand All @@ -23,9 +24,25 @@ export function ProjectSelector(props: {
currentOrganizationSlug: string;
currentProjectSlug: string;
organizations: FragmentType<typeof ProjectSelector_OrganizationConnectionFragment> | null;
onValueChange?: ((value: string) => void) | undefined;
optional?: boolean;
showOrganization?: boolean;
}) {
const router = useRouter();

const optional = typeof props.optional !== 'undefined' ? props.optional : false;
Copy link
Contributor

Choose a reason for hiding this comment

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

This would be shorted when using nullish coalescing

Suggested change
const optional = typeof props.optional !== 'undefined' ? props.optional : false;
const optional = props.optional ?? false;

const showOrganization =
typeof props.showOrganization !== 'undefined' ? props.showOrganization : true;
Comment on lines +33 to +34
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
const showOrganization =
typeof props.showOrganization !== 'undefined' ? props.showOrganization : true;
const showOrganization = props.showOrganization ?? true

const defaultFunc = (id: string) => {
void router.navigate({
to: '/$organizationSlug/$projectSlug',
params: {
organizationSlug: props.currentOrganizationSlug,
projectSlug: id,
},
});
};
const onValueChangeFunc =
typeof props.onValueChange !== 'undefined' ? props.onValueChange : defaultFunc;
const organizations = useFragment(
ProjectSelector_OrganizationConnectionFragment,
props.organizations,
Expand All @@ -42,47 +59,51 @@ export function ProjectSelector(props: {

return (
<>
{currentOrganization ? (
<Link
to="/$organizationSlug"
params={{ organizationSlug: props.currentOrganizationSlug }}
className="max-w-[200px] shrink-0 truncate font-medium"
>
{currentOrganization.slug}
</Link>
{showOrganization ? (
currentOrganization ? (
<Link
to="/$organizationSlug"
params={{ organizationSlug: props.currentOrganizationSlug }}
className="max-w-[200px] shrink-0 truncate font-medium"
>
{currentOrganization.slug}
</Link>
) : (
<div className="h-5 w-48 max-w-[200px] animate-pulse rounded-full bg-gray-800" />
)
) : (
<div className="h-5 w-48 max-w-[200px] animate-pulse rounded-full bg-gray-800" />
''
)}
{projectEdges?.length && currentProject ? (
{(projectEdges?.length && currentProject) || optional ? (
<>
<div className="italic text-gray-500">/</div>
<Select
value={props.currentProjectSlug}
onValueChange={id => {
void router.navigate({
to: '/$organizationSlug/$projectSlug',
params: {
organizationSlug: props.currentOrganizationSlug,
projectSlug: id,
},
});
}}
>
{showOrganization ? <div className="italic text-gray-500">/</div> : <></>}
<Select value={props.currentProjectSlug} onValueChange={onValueChangeFunc}>
<SelectTrigger variant="default" data-cy="project-picker-trigger">
<div className="font-medium" data-cy="project-picker-current">
{currentProject.slug}
{optional ? (
<SelectValue placeholder="Pick an option" />
) : (
(currentProject?.slug ?? '')
)}
</div>
</SelectTrigger>
<SelectContent>
{projectEdges.map(edge => (
<SelectItem
key={edge.node.slug}
value={edge.node.slug}
data-cy={`project-picker-option-${edge.node.slug}`}
>
{edge.node.slug}
{optional ? (
<SelectItem key="empty" value="empty" data-cy="project-picker-option-Unassigned">
Unassigned
</SelectItem>
))}
) : null}
{projectEdges?.map(edge => {
return (
<SelectItem
key={edge.node.slug}
value={edge.node.slug}
data-cy={`project-picker-option-${edge.node.slug}`}
>
{edge.node.slug}
</SelectItem>
);
})}
</SelectContent>
</Select>
</>
Expand Down
Loading
Loading