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 2 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
13 changes: 13 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!
project: String
target: String
category: SupportCategory!
subject: String!
description: String!
priority: SupportTicketPriority!
Expand Down Expand Up @@ -71,6 +74,9 @@ export default gql`
id: ID!
status: SupportTicketStatus!
priority: SupportTicketPriority!
category: SupportCategory!
project: String
target: String
createdAt: DateTime!
updatedAt: DateTime!
subject: String!
Expand All @@ -95,6 +101,13 @@ export default gql`
fromSupport: Boolean!
}

enum SupportCategory {
TECHNICAL_ISSUE
BILLING
COMPLIANCE
OTHER
}

enum SupportTicketPriority {
NORMAL
HIGH
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Logger } from '../../shared/providers/logger';
import { Storage } from '../../shared/providers/storage';
import { OrganizationManager } from './../../organization/providers/organization-manager';
import { SUPPORT_MODULE_CONFIG, type SupportConfig } from './config';
import { SupportCategory } from 'packages/libraries/core/src/client/__generated__/types';

export const SupportTicketPriorityAPIModel = z.enum(['low', 'normal', 'high', 'urgent']);
export const SupportTicketStatusAPIModel = z.enum([
Expand All @@ -23,6 +24,7 @@ export const SupportTicketStatusAPIModel = z.enum([

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

const SupportTicketModel = z.object({
id: z.number(),
Expand All @@ -45,6 +47,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 +85,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
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@ import type { SupportTicketResolvers } from './../../../__generated__/types';

export const SupportTicket: SupportTicketResolvers = {
comments: async (ticket, _args, { injector }) => {
return {
edges: [],
pageInfo: {
endCursor: '',
hasNextPage: false,
hasPreviousPage: false,
startCursor: '',
},
};
const response = await injector.get(SupportManager).getTicketComments(ticket.id);

return {
Expand Down
90 changes: 63 additions & 27 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 @@ -21,7 +22,15 @@ export function ProjectSelector(props: {
currentOrganizationSlug: string;
currentProjectSlug: string;
organizations: FragmentType<typeof ProjectSelector_OrganizationConnectionFragment> | null;
onValueChange?: Function;
optional?: boolean;
showOrganization?: boolean;
}) {
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 onValueChangeFunc =
typeof props.onValueChange !== undefined ? props.onValueChange : undefined;
const router = useRouter();

const organizations = useFragment(
Expand All @@ -38,47 +47,74 @@ 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" />
''
)}
{projects?.length && currentProject ? (

{(projects?.length && currentProject) || optional ? (
<>
<div className="italic text-gray-500">/</div>
{showOrganization ? <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,
},
});
}}
onValueChange={
onValueChangeFunc
? id => {
onValueChangeFunc(id);
}
: id => {
void router.navigate({
to: '/$organizationSlug/$projectSlug',
params: {
organizationSlug: props.currentOrganizationSlug,
projectSlug: id,
},
});
}
}
>
<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 && !optional ? currentProject.slug : ''}
</div>
</SelectTrigger>
<SelectContent>
{projects.map(project => (
{optional ? (
<SelectItem
key={project.slug}
value={project.slug}
data-cy={`project-picker-option-${project.slug}`}
key={'empty'}
value={'empty'}
data-cy={`project-picker-option-Unassigned`}
>
{project.slug}
Unassigned
</SelectItem>
))}
) : (
<></>
)}
{projects ? (
projects.map(project => (
<SelectItem
key={project.slug}
value={project.slug}
data-cy={`project-picker-option-${project.slug}`}
>
{project.slug}
</SelectItem>
))
) : (
<></>
)}
</SelectContent>
</Select>
</>
Expand Down
137 changes: 92 additions & 45 deletions packages/web/app/src/components/layouts/target-selector.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { Select, SelectContent, SelectItem, SelectTrigger } from '@/components/ui/select';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { FragmentType, graphql, useFragment } from '@/gql';
import { Link, useRouter } from '@tanstack/react-router';

Expand Down Expand Up @@ -27,10 +33,19 @@ export function TargetSelector(props: {
currentOrganizationSlug: string;
currentProjectSlug: string;
currentTargetSlug: string;
optional?: boolean;
showOrganization?: boolean;
showProject?: boolean;
onValueChange?: Function;
organizations: FragmentType<typeof TargetSelector_OrganizationConnectionFragment> | null;
}) {
const router = useRouter();

const showOrganization =
typeof props.showOrganization !== 'undefined' ? props.showOrganization : true;
const showProject = typeof props.showProject !== 'undefined' ? props.showProject : true;
const isOptional = typeof props.optional !== undefined ? props.optional : false;

const organizations = useFragment(
TargetSelector_OrganizationConnectionFragment,
props.organizations,
Expand All @@ -43,70 +58,102 @@ export function TargetSelector(props: {
const projects = currentOrganization?.projects.nodes;
const currentProject = projects?.find(node => node.slug === props.currentProjectSlug);

const targets = currentProject?.targets.nodes;
const targets = currentProject?.targets?.nodes;
const currentTarget = targets?.find(node => node.slug === props.currentTargetSlug);
const onValueChangeFunc =
typeof props.onValueChange !== undefined ? props.onValueChange : () => {};

return (
<>
{currentOrganization ? (
<Link
to="/$organizationSlug"
params={{
organizationSlug: currentOrganization.slug,
}}
className="max-w-[200px] shrink-0 truncate font-medium"
>
{currentOrganization.slug}
</Link>
{showOrganization ? (
currentOrganization ? (
<Link
to="/$organizationSlug"
params={{
organizationSlug: currentOrganization.slug,
}}
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" />
<></>
)}
<div className="italic text-gray-500">/</div>
{currentOrganization && currentProject ? (
<Link
to="/$organizationSlug/$projectSlug"
params={{
organizationSlug: props.currentOrganizationSlug,
projectSlug: props.currentProjectSlug,
}}
className="max-w-[200px] shrink-0 truncate font-medium"
>
{currentProject.slug}
</Link>
{showOrganization ? <div className="italic text-gray-500">/</div> : <></>}
{showProject ? (
currentOrganization && currentProject ? (
<Link
to="/$organizationSlug/$projectSlug"
params={{
organizationSlug: props.currentOrganizationSlug,
projectSlug: props.currentProjectSlug,
}}
className="max-w-[200px] shrink-0 truncate font-medium"
>
{currentProject.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" />
<></>
)}
<div className="italic text-gray-500">/</div>
{targets?.length && currentOrganization && currentProject && currentTarget ? (
{showProject ? <div className="italic text-gray-500">/</div> : <></>}
{(targets?.length && currentOrganization && currentProject && currentTarget) || isOptional ? (
<>
<Select
value={props.currentTargetSlug}
onValueChange={id => {
void router.navigate({
to: '/$organizationSlug/$projectSlug/$targetSlug',
params: {
organizationSlug: props.currentOrganizationSlug,
projectSlug: props.currentProjectSlug,
targetSlug: id,
},
});
}}
onValueChange={
onValueChangeFunc
? id => {
onValueChangeFunc(id);
}
: id => {
void router.navigate({
to: '/$organizationSlug/$projectSlug/$targetSlug',
params: {
organizationSlug: props.currentOrganizationSlug,
projectSlug: props.currentProjectSlug,
targetSlug: id,
},
});
}
}
>
<SelectTrigger variant="default" data-cy="target-picker-trigger">
<div className="font-medium" data-cy="target-picker-current">
{currentTarget.slug}
{isOptional ? <SelectValue placeholder="Pick an option" /> : ''}
{currentTarget && !isOptional ? currentTarget.slug : ''}{' '}
</div>
</SelectTrigger>
<SelectContent>
{targets.map(target => (
{isOptional ? (
<SelectItem
key={target.slug}
value={target.slug}
data-cy={`target-picker-option-${target.slug}`}
key={'empty'}
value={'empty'}
data-cy={`project-picker-option-Unassigned`}
>
{target.slug}
Unassigned
</SelectItem>
))}
) : (
<></>
)}
{targets ? (
targets.map(target => (
<SelectItem
key={target.slug}
value={target.slug}
data-cy={`target-picker-option-${target.slug}`}
>
{target.slug}
</SelectItem>
))
) : (
<></>
)}
</SelectContent>
</Select>
</>
Expand Down
Loading