From 3be1fcf52eb6928a9a5fc6fe7468f8dafcba7a4e Mon Sep 17 00:00:00 2001 From: Emily Goodwin Date: Tue, 1 Apr 2025 19:08:21 -0400 Subject: [PATCH 01/12] update support ticket to support categories, project, and target options --- .../api/src/modules/support/module.graphql.ts | 13 ++ .../support/providers/support-manager.ts | 8 + .../support/resolvers/SupportTicket.ts | 9 ++ .../components/layouts/project-selector.tsx | 90 ++++++++---- .../components/layouts/target-selector.tsx | 137 ++++++++++++------ .../app/src/pages/organization-support.tsx | 120 ++++++++++++++- 6 files changed, 302 insertions(+), 75 deletions(-) diff --git a/packages/services/api/src/modules/support/module.graphql.ts b/packages/services/api/src/modules/support/module.graphql.ts index de34b54ae9..e6cfb51b6b 100644 --- a/packages/services/api/src/modules/support/module.graphql.ts +++ b/packages/services/api/src/modules/support/module.graphql.ts @@ -30,6 +30,9 @@ export default gql` input SupportTicketCreateInput { organizationSlug: String! + project: String + target: String + category: SupportCategory! subject: String! description: String! priority: SupportTicketPriority! @@ -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! @@ -95,6 +101,13 @@ export default gql` fromSupport: Boolean! } + enum SupportCategory { + TECHNICAL_ISSUE + BILLING + COMPLIANCE + OTHER + } + enum SupportTicketPriority { NORMAL HIGH diff --git a/packages/services/api/src/modules/support/providers/support-manager.ts b/packages/services/api/src/modules/support/providers/support-manager.ts index cc96d9ba48..89469deb40 100644 --- a/packages/services/api/src/modules/support/providers/support-manager.ts +++ b/packages/services/api/src/modules/support/providers/support-manager.ts @@ -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([ @@ -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(), @@ -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(), @@ -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, diff --git a/packages/services/api/src/modules/support/resolvers/SupportTicket.ts b/packages/services/api/src/modules/support/resolvers/SupportTicket.ts index 1cd012f17a..b7c1f1a4fb 100644 --- a/packages/services/api/src/modules/support/resolvers/SupportTicket.ts +++ b/packages/services/api/src/modules/support/resolvers/SupportTicket.ts @@ -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 { diff --git a/packages/web/app/src/components/layouts/project-selector.tsx b/packages/web/app/src/components/layouts/project-selector.tsx index cd2d5b815d..a6003579ee 100644 --- a/packages/web/app/src/components/layouts/project-selector.tsx +++ b/packages/web/app/src/components/layouts/project-selector.tsx @@ -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(` @@ -21,7 +22,15 @@ export function ProjectSelector(props: { currentOrganizationSlug: string; currentProjectSlug: string; organizations: FragmentType | null; + onValueChange?: Function; + optional?: boolean; + showOrganization?: boolean; }) { + const optional = typeof props.optional !== 'undefined' ? props.optional : false; + const showOrganization = + typeof props.showOrganization !== 'undefined' ? props.showOrganization : true; + const onValueChangeFunc = + typeof props.onValueChange !== undefined ? props.onValueChange : undefined; const router = useRouter(); const organizations = useFragment( @@ -38,47 +47,74 @@ export function ProjectSelector(props: { return ( <> - {currentOrganization ? ( - - {currentOrganization.slug} - + {showOrganization ? ( + currentOrganization ? ( + + {currentOrganization.slug} + + ) : ( +
+ ) ) : ( -
+ '' )} - {projects?.length && currentProject ? ( + + {(projects?.length && currentProject) || optional ? ( <> -
/
+ {showOrganization ?
/
: <>} diff --git a/packages/web/app/src/components/layouts/target-selector.tsx b/packages/web/app/src/components/layouts/target-selector.tsx index 06723f4a2e..6bece927cd 100644 --- a/packages/web/app/src/components/layouts/target-selector.tsx +++ b/packages/web/app/src/components/layouts/target-selector.tsx @@ -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'; @@ -27,10 +33,19 @@ export function TargetSelector(props: { currentOrganizationSlug: string; currentProjectSlug: string; currentTargetSlug: string; + optional?: boolean; + showOrganization?: boolean; + showProject?: boolean; + onValueChange?: Function; organizations: FragmentType | 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, @@ -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 ? ( - - {currentOrganization.slug} - + {showOrganization ? ( + currentOrganization ? ( + + {currentOrganization.slug} + + ) : ( +
+ ) ) : ( -
+ <> )} -
/
- {currentOrganization && currentProject ? ( - - {currentProject.slug} - + {showOrganization ?
/
: <>} + {showProject ? ( + currentOrganization && currentProject ? ( + + {currentProject.slug} + + ) : ( +
+ ) ) : ( -
+ <> )} -
/
- {targets?.length && currentOrganization && currentProject && currentTarget ? ( + {showProject ?
/
: <>} + {(targets?.length && currentOrganization && currentProject && currentTarget) || isOptional ? ( <> diff --git a/packages/web/app/src/pages/organization-support.tsx b/packages/web/app/src/pages/organization-support.tsx index 61e717aa58..4aa854bddc 100644 --- a/packages/web/app/src/pages/organization-support.tsx +++ b/packages/web/app/src/pages/organization-support.tsx @@ -1,9 +1,11 @@ -import { useCallback } from 'react'; +import { useCallback, useState } from 'react'; import { PencilIcon } from 'lucide-react'; import { useForm } from 'react-hook-form'; import { useMutation, useQuery } from 'urql'; import { z } from 'zod'; import { OrganizationLayout, Page } from '@/components/layouts/organization'; +import { ProjectSelector } from '@/components/layouts/project-selector'; +import { TargetSelector } from '@/components/layouts/target-selector'; import { Priority, priorityDescription, Status } from '@/components/organization/support'; import { Button } from '@/components/ui/button'; import { @@ -20,6 +22,7 @@ import { Meta } from '@/components/ui/meta'; import { Subtitle, Title } from '@/components/ui/page'; import { QueryError } from '@/components/ui/query-error'; import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; +import { Select, SelectContent, SelectItem, SelectTrigger } from '@/components/ui/select'; import { Sheet, SheetContent, @@ -40,16 +43,30 @@ import { Textarea } from '@/components/ui/textarea'; import { TimeAgo } from '@/components/ui/time-ago'; import { TooltipProvider } from '@/components/ui/tooltip'; import { FragmentType, graphql, useFragment } from '@/gql'; -import { SupportTicketPriority, SupportTicketStatus } from '@/gql/graphql'; +import { SupportCategory, SupportTicketPriority, SupportTicketStatus } from '@/gql/graphql'; import { useNotifications, useToggle } from '@/lib/hooks'; import { cn } from '@/lib/utils'; import { zodResolver } from '@hookform/resolvers/zod'; import { Link } from '@tanstack/react-router'; +const NewTicketQuery = graphql(` + query NewTicketQuery { + organizations { + ...ProjectSelector_OrganizationConnectionFragment + ...TargetSelector_OrganizationConnectionFragment + } + } +`); + const newTicketFormSchema = z.object({ subject: z.string().min(2, { message: 'Subject must be at least 2 characters.', }), + category: z.nativeEnum(SupportCategory, { + required_error: 'A priority is required.', + }), + project: z.string().optional(), + target: z.string().optional(), priority: z.nativeEnum(SupportTicketPriority, { required_error: 'A priority is required.', }), @@ -79,17 +96,26 @@ function NewTicketForm(props: { onClose: () => void; onSubmit: () => void; }) { + const [project, setProject] = useState(''); + + const [query] = useQuery({ + query: NewTicketQuery, + requestPolicy: 'cache-first', + }); + const notify = useNotifications(); const form = useForm({ resolver: zodResolver(newTicketFormSchema), defaultValues: { subject: '', + category: SupportCategory.Other, priority: SupportTicketPriority.Normal, description: '', }, }); const [_, mutate] = useMutation(NewTicketForm_SupportTicketCreateMutation); + const [selectedProject, setSelectedProject] = ''; const onClose = useCallback(() => { form.reset({ subject: '', @@ -104,6 +130,9 @@ function NewTicketForm(props: { const result = await mutate({ input: { organizationSlug: props.organizationSlug, + category: data.category, + project: data.project !== 'empty' ? data.project : undefined, + target: data.target !== 'empty' ? data.target : undefined, subject: data.subject, priority: data.priority, description: data.description, @@ -139,7 +168,7 @@ function NewTicketForm(props: { } }} > - +
)} /> + ( + + Category + + + + + + )} + /> + ( + + Project (Optional) + + { + field.onChange(value); + setProject(value); + }} + organizations={ + query?.data?.organizations ? query.data.organizations : null + } + optional={true} + showOrganization={false} + /> + + + + )} + /> + ( + + Target (Optional) + + + + + )} + /> + Date: Thu, 3 Apr 2025 11:44:16 -0400 Subject: [PATCH 02/12] address comments --- .../api/src/modules/support/module.graphql.ts | 2 +- .../support/resolvers/SupportTicket.ts | 9 ---- .../components/layouts/project-selector.tsx | 46 ++++++++----------- .../components/layouts/target-selector.tsx | 15 +++--- .../app/src/pages/organization-support.tsx | 4 +- 5 files changed, 28 insertions(+), 48 deletions(-) diff --git a/packages/services/api/src/modules/support/module.graphql.ts b/packages/services/api/src/modules/support/module.graphql.ts index e6cfb51b6b..34184843c2 100644 --- a/packages/services/api/src/modules/support/module.graphql.ts +++ b/packages/services/api/src/modules/support/module.graphql.ts @@ -32,7 +32,7 @@ export default gql` organizationSlug: String! project: String target: String - category: SupportCategory! + category: SupportCategory subject: String! description: String! priority: SupportTicketPriority! diff --git a/packages/services/api/src/modules/support/resolvers/SupportTicket.ts b/packages/services/api/src/modules/support/resolvers/SupportTicket.ts index b7c1f1a4fb..1cd012f17a 100644 --- a/packages/services/api/src/modules/support/resolvers/SupportTicket.ts +++ b/packages/services/api/src/modules/support/resolvers/SupportTicket.ts @@ -3,15 +3,6 @@ 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 { diff --git a/packages/web/app/src/components/layouts/project-selector.tsx b/packages/web/app/src/components/layouts/project-selector.tsx index a6003579ee..b8e66a7bc3 100644 --- a/packages/web/app/src/components/layouts/project-selector.tsx +++ b/packages/web/app/src/components/layouts/project-selector.tsx @@ -2,6 +2,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger } from '@/components/u import { FragmentType, graphql, useFragment } from '@/gql'; import { SelectValue } from '@radix-ui/react-select'; import { Link, useRouter } from '@tanstack/react-router'; +import { SetStateAction } from 'react'; const ProjectSelector_OrganizationConnectionFragment = graphql(` fragment ProjectSelector_OrganizationConnectionFragment on OrganizationConnection { @@ -22,15 +23,23 @@ export function ProjectSelector(props: { currentOrganizationSlug: string; currentProjectSlug: string; organizations: FragmentType | null; - onValueChange?: Function; + onValueChange?: (id: string) => void; optional?: boolean; showOrganization?: boolean; }) { const optional = typeof props.optional !== 'undefined' ? props.optional : false; const showOrganization = typeof props.showOrganization !== 'undefined' ? props.showOrganization : true; - const onValueChangeFunc = - typeof props.onValueChange !== undefined ? props.onValueChange : undefined; + const onValueChangeFunc: (id: string) => void = + typeof props.onValueChange !== 'undefined' ? props.onValueChange : (id: string) => { + void router.navigate({ + to: '/$organizationSlug/$projectSlug', + params: { + organizationSlug: props.currentOrganizationSlug, + projectSlug: id, + }, + }) + }; const router = useRouter(); const organizations = useFragment( @@ -65,29 +74,14 @@ export function ProjectSelector(props: { {(projects?.length && currentProject) || optional ? ( <> - {showOrganization ?
/
: <>} + {showOrganization ?
/
: null} diff --git a/packages/web/app/src/components/layouts/target-selector.tsx b/packages/web/app/src/components/layouts/target-selector.tsx index 6bece927cd..3e22752d01 100644 --- a/packages/web/app/src/components/layouts/target-selector.tsx +++ b/packages/web/app/src/components/layouts/target-selector.tsx @@ -79,9 +79,8 @@ export function TargetSelector(props: { ) : (
) - ) : ( - <> - )} + ) : null + } {showOrganization ?
/
: <>} {showProject ? ( currentOrganization && currentProject ? ( @@ -98,10 +97,9 @@ export function TargetSelector(props: { ) : (
) - ) : ( - <> - )} - {showProject ?
/
: <>} + ) : null + } + {showProject ?
/
: null} {(targets?.length && currentOrganization && currentProject && currentTarget) || isOptional ? ( <>
@@ -247,7 +247,7 @@ function NewTicketForm(props: {
- {Object.values(SupportCategory).map(category => ( + {Object.values(SupportCategoryType).map(category => ( Date: Thu, 15 May 2025 16:14:35 -0400 Subject: [PATCH 06/12] update to fix lint errors --- .../app/src/components/layouts/project-selector.tsx | 7 +++---- .../app/src/components/layouts/target-selector.tsx | 10 +++++----- packages/web/app/src/pages/organization-support.tsx | 13 ++++++------- 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/packages/web/app/src/components/layouts/project-selector.tsx b/packages/web/app/src/components/layouts/project-selector.tsx index 510462367e..a0a42a1d19 100644 --- a/packages/web/app/src/components/layouts/project-selector.tsx +++ b/packages/web/app/src/components/layouts/project-selector.tsx @@ -2,7 +2,6 @@ import { Select, SelectContent, SelectItem, SelectTrigger } from '@/components/u import { FragmentType, graphql, useFragment } from '@/gql'; import { SelectValue } from '@radix-ui/react-select'; import { Link, useRouter } from '@tanstack/react-router'; -import { SetStateAction } from 'react'; const ProjectSelector_OrganizationConnectionFragment = graphql(` fragment ProjectSelector_OrganizationConnectionFragment on OrganizationConnection { @@ -87,9 +86,9 @@ export function ProjectSelector(props: { {optional ? ( Unassigned diff --git a/packages/web/app/src/components/layouts/target-selector.tsx b/packages/web/app/src/components/layouts/target-selector.tsx index 05af7cae9b..e0575390d4 100644 --- a/packages/web/app/src/components/layouts/target-selector.tsx +++ b/packages/web/app/src/components/layouts/target-selector.tsx @@ -44,7 +44,7 @@ export function TargetSelector(props: { 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 isOptional = typeof props.optional !== 'undefined' ? props.optional : false; const organizations = useFragment( TargetSelector_OrganizationConnectionFragment, @@ -61,7 +61,7 @@ export function TargetSelector(props: { const targets = currentProject?.targets?.nodes; const currentTarget = targets?.find(node => node.slug === props.currentTargetSlug); const onValueChangeFunc = - typeof props.onValueChange !== undefined ? props.onValueChange : () => {}; + typeof props.onValueChange !== 'undefined' ? props.onValueChange : () => {}; return ( <> @@ -129,9 +129,9 @@ export function TargetSelector(props: { {isOptional ? ( Unassigned diff --git a/packages/web/app/src/pages/organization-support.tsx b/packages/web/app/src/pages/organization-support.tsx index 78b0c60e17..2adc53ed62 100644 --- a/packages/web/app/src/pages/organization-support.tsx +++ b/packages/web/app/src/pages/organization-support.tsx @@ -115,7 +115,6 @@ function NewTicketForm(props: { }); const [_, mutate] = useMutation(NewTicketForm_SupportTicketCreateMutation); - const [selectedProject, setSelectedProject] = ''; const onClose = useCallback(() => { form.reset({ subject: '', @@ -273,15 +272,15 @@ function NewTicketForm(props: { ) => { field.onChange(value); setProject(value); }} organizations={ - query?.data?.organizations ? query.data.organizations : null + query?.data?.organizations || null } - optional={true} + optional showOrganization={false} /> @@ -298,13 +297,13 @@ function NewTicketForm(props: { From 9a162e5914c64511bdae62ec9240d3d87af34085 Mon Sep 17 00:00:00 2001 From: Emily Goodwin Date: Mon, 19 May 2025 15:42:14 -0400 Subject: [PATCH 07/12] fix SupportCategoryType typecheck errors --- codegen.mts | 1 + .../api/src/modules/support/module.graphql.mappers.ts | 3 ++- .../src/modules/support/providers/support-manager.ts | 6 +++--- .../support/resolvers/Mutation/supportTicketCreate.ts | 1 + .../modules/support/resolvers/SupportCategoryType.ts | 9 +++++++++ packages/services/api/src/shared/entities.ts | 10 +++++----- packages/web/app/src/pages/organization-support.tsx | 5 ++--- 7 files changed, 23 insertions(+), 12 deletions(-) create mode 100644 packages/services/api/src/modules/support/resolvers/SupportCategoryType.ts diff --git a/codegen.mts b/codegen.mts index 6adbbfaea7..7458fc946a 100644 --- a/codegen.mts +++ b/codegen.mts @@ -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, diff --git a/packages/services/api/src/modules/support/module.graphql.mappers.ts b/packages/services/api/src/modules/support/module.graphql.mappers.ts index 4d2c627e6d..78d5bbd2ca 100644 --- a/packages/services/api/src/modules/support/module.graphql.mappers.ts +++ b/packages/services/api/src/modules/support/module.graphql.mappers.ts @@ -1,4 +1,5 @@ -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; \ No newline at end of file diff --git a/packages/services/api/src/modules/support/providers/support-manager.ts b/packages/services/api/src/modules/support/providers/support-manager.ts index b85f5e9e65..1a1ae8fc40 100644 --- a/packages/services/api/src/modules/support/providers/support-manager.ts +++ b/packages/services/api/src/modules/support/providers/support-manager.ts @@ -3,7 +3,7 @@ import { Inject, Injectable, Scope } from 'graphql-modules'; import { z } from 'zod'; import { Organization, - SupportCategory, + SupportCategoryType, SupportTicketPriority, SupportTicketStatus, } from '../../../shared/entities'; @@ -28,7 +28,7 @@ export const SupportTicketStatusAPIModel = z.enum([ export const SupportTicketPriorityModel = z.nativeEnum(SupportTicketPriority); export const SupportTicketStatusModel = z.nativeEnum(SupportTicketStatus); -export const SupportTicketCategoryModel = z.nativeEnum(SupportCategory).optional(); +export const SupportTicketCategoryModel = z.nativeEnum(SupportCategoryType).optional(); const SupportTicketModel = z.object({ id: z.number(), @@ -520,7 +520,7 @@ export class SupportManager { organizationId: string; subject: string; description: string; - category?: SupportCategory; + category?: z.infer; project?: string; target?: string; priority: z.infer; diff --git a/packages/services/api/src/modules/support/resolvers/Mutation/supportTicketCreate.ts b/packages/services/api/src/modules/support/resolvers/Mutation/supportTicketCreate.ts index 79433769ff..9b18b8a064 100644 --- a/packages/services/api/src/modules/support/resolvers/Mutation/supportTicketCreate.ts +++ b/packages/services/api/src/modules/support/resolvers/Mutation/supportTicketCreate.ts @@ -11,6 +11,7 @@ export const supportTicketCreate: NonNullable
- {field.value.charAt(0) + - field.value.substring(1).toLocaleLowerCase().replace('_', ' ')} + {field.value}
From 24bf245f1c5103be16125d2b269f691d42c70f0c Mon Sep 17 00:00:00 2001 From: Emily Goodwin Date: Sun, 22 Jun 2025 20:51:26 -0400 Subject: [PATCH 08/12] pnpm prettier and lint fixes --- .../modules/support/module.graphql.mappers.ts | 8 +- .../support/providers/support-manager.ts | 8 +- .../support/resolvers/SupportCategoryType.ts | 2 +- .../support/resolvers/SupportTicket.ts | 9 ++ .../components/layouts/project-selector.tsx | 57 +++----- .../app/src/components/layouts/project.tsx | 6 +- .../components/layouts/target-selector.tsx | 127 +++++------------- .../app/src/pages/organization-support.tsx | 10 +- 8 files changed, 83 insertions(+), 144 deletions(-) diff --git a/packages/services/api/src/modules/support/module.graphql.mappers.ts b/packages/services/api/src/modules/support/module.graphql.mappers.ts index 78d5bbd2ca..fd18017217 100644 --- a/packages/services/api/src/modules/support/module.graphql.mappers.ts +++ b/packages/services/api/src/modules/support/module.graphql.mappers.ts @@ -1,5 +1,9 @@ -import { SupportCategoryType, SupportTicketPriority, SupportTicketStatus } from '../../shared/entities'; +import { + SupportCategoryType, + SupportTicketPriority, + SupportTicketStatus, +} from '../../shared/entities'; export type SupportTicketPriorityMapper = SupportTicketPriority; export type SupportTicketStatusMapper = SupportTicketStatus; -export type SupportCategoryTypeMapper = SupportCategoryType; \ No newline at end of file +export type SupportCategoryTypeMapper = SupportCategoryType; diff --git a/packages/services/api/src/modules/support/providers/support-manager.ts b/packages/services/api/src/modules/support/providers/support-manager.ts index 1a1ae8fc40..86b72ca806 100644 --- a/packages/services/api/src/modules/support/providers/support-manager.ts +++ b/packages/services/api/src/modules/support/providers/support-manager.ts @@ -28,7 +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).optional(); +export const SupportTicketCategoryModel = z.nativeEnum(SupportCategoryType); const SupportTicketModel = z.object({ id: z.number(), @@ -578,9 +578,9 @@ 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 + 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} `; diff --git a/packages/services/api/src/modules/support/resolvers/SupportCategoryType.ts b/packages/services/api/src/modules/support/resolvers/SupportCategoryType.ts index 2547160001..7c6c49d2c8 100644 --- a/packages/services/api/src/modules/support/resolvers/SupportCategoryType.ts +++ b/packages/services/api/src/modules/support/resolvers/SupportCategoryType.ts @@ -1,5 +1,5 @@ -import type { SupportCategoryTypeResolvers } from './../../../__generated__/types'; import { SupportCategoryType as SupportCategoryTypeEnum } from '../../../shared/entities'; +import type { SupportCategoryTypeResolvers } from './../../../__generated__/types'; export const SupportCategoryType: SupportCategoryTypeResolvers = { TECHNICAL_ISSUE: SupportCategoryTypeEnum.TECHNICAL_ISSUE, diff --git a/packages/services/api/src/modules/support/resolvers/SupportTicket.ts b/packages/services/api/src/modules/support/resolvers/SupportTicket.ts index 1cd012f17a..b7c1f1a4fb 100644 --- a/packages/services/api/src/modules/support/resolvers/SupportTicket.ts +++ b/packages/services/api/src/modules/support/resolvers/SupportTicket.ts @@ -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 { diff --git a/packages/web/app/src/components/layouts/project-selector.tsx b/packages/web/app/src/components/layouts/project-selector.tsx index 2142c0f9b9..8110f28670 100644 --- a/packages/web/app/src/components/layouts/project-selector.tsx +++ b/packages/web/app/src/components/layouts/project-selector.tsx @@ -1,7 +1,7 @@ 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'; +import { Link } from '@tanstack/react-router'; const ProjectSelector_OrganizationConnectionFragment = graphql(` fragment ProjectSelector_OrganizationConnectionFragment on OrganizationConnection { @@ -24,24 +24,15 @@ export function ProjectSelector(props: { currentOrganizationSlug: string; currentProjectSlug: string; organizations: FragmentType | null; - onValueChange?: (id: string) => void; + onValueChange?: Function; optional?: boolean; showOrganization?: boolean; }) { const optional = typeof props.optional !== 'undefined' ? props.optional : false; const showOrganization = typeof props.showOrganization !== 'undefined' ? props.showOrganization : true; - const onValueChangeFunc: (id: string) => void = - typeof props.onValueChange !== 'undefined' ? props.onValueChange : (id: string) => { - void router.navigate({ - to: '/$organizationSlug/$projectSlug', - params: { - organizationSlug: props.currentOrganizationSlug, - projectSlug: id, - }, - }) - }; - const router = useRouter(); + const onValueChangeFunc = + typeof props.onValueChange !== 'undefined' ? props.onValueChange : undefined; const organizations = useFragment( ProjectSelector_OrganizationConnectionFragment, @@ -76,39 +67,29 @@ export function ProjectSelector(props: { )} {projectEdges?.length && currentProject ? ( <> - {showOrganization ?
/
: null} -
- {optional ? : (currentProject?.slug ?? '')} + {optional ? ( + + ) : ( + (currentProject?.slug ?? '') + )}
- {projectEdges.map(edge => ( - - {edge.node.slug} - - ) : null - } - {projects ? ( - projects.map(project => ( + {projectEdges.map(edge => { + return ( - {project.slug} + {edge.node.slug} - )) - ) : null - } + ); + })} diff --git a/packages/web/app/src/components/layouts/project.tsx b/packages/web/app/src/components/layouts/project.tsx index 53754a339c..b30b4b25d9 100644 --- a/packages/web/app/src/components/layouts/project.tsx +++ b/packages/web/app/src/components/layouts/project.tsx @@ -95,7 +95,11 @@ export function ProjectLayout({ ) ?? null + } />
diff --git a/packages/web/app/src/components/layouts/target-selector.tsx b/packages/web/app/src/components/layouts/target-selector.tsx index 2b1ed152ea..90fea1a19d 100644 --- a/packages/web/app/src/components/layouts/target-selector.tsx +++ b/packages/web/app/src/components/layouts/target-selector.tsx @@ -1,12 +1,5 @@ -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@/components/ui/select'; import { FragmentType, graphql, useFragment } from '@/gql'; -import { Link, useRouter } from '@tanstack/react-router'; +import { Link } from '../ui/link'; const TargetSelector_OrganizationConnectionFragment = graphql(` fragment TargetSelector_OrganizationConnectionFragment on OrganizationConnection { @@ -40,15 +33,12 @@ export function TargetSelector(props: { optional?: boolean; showOrganization?: boolean; showProject?: boolean; - onValueChange?: Function; + onValueChange?: (value: string) => void; organizations: FragmentType | 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, @@ -63,93 +53,48 @@ export function TargetSelector(props: { const currentProject = projects?.find(edge => edge.node.slug === props.currentProjectSlug)?.node; const targetEdges = currentProject?.targets.edges; - const currentTarget = targetEdges?.find(edge => edge.node.slug === props.currentTargetSlug)?.node; return ( <> - {currentOrganization ? ( - - {currentOrganization.slug} - + {showOrganization ? ( + currentOrganization ? ( + + {currentOrganization.slug} + + ) : ( +
+ ) ) : ( -
+ <> )} -
/
- {currentOrganization && currentProject ? ( - - {currentProject.slug} - + {showOrganization ?
/
: <>} + {showProject ? ( + currentOrganization && currentProject ? ( + + {currentProject.slug} + + ) : ( +
+ ) ) : ( -
+ <> )}
/
- {targetEdges?.length && currentOrganization && currentProject && currentTarget ? ( - <> - - + {targetEdges?.length ? ( + ) : (
)} diff --git a/packages/web/app/src/pages/organization-support.tsx b/packages/web/app/src/pages/organization-support.tsx index 18cfe5c21a..b1a12617d2 100644 --- a/packages/web/app/src/pages/organization-support.tsx +++ b/packages/web/app/src/pages/organization-support.tsx @@ -276,9 +276,7 @@ function NewTicketForm(props: { field.onChange(value); setProject(value); }} - organizations={ - query?.data?.organizations || null - } + organizations={query?.data?.organizations || null} optional showOrganization={false} /> @@ -295,9 +293,7 @@ function NewTicketForm(props: { Target (Optional) - {(tickets ?? []).map(ticket => ( + {tickets?.map(ticket => ( Date: Sun, 22 Jun 2025 22:27:58 -0400 Subject: [PATCH 09/12] fix merge issues --- .../support/resolvers/SupportTicket.ts | 9 --- .../components/layouts/project-selector.tsx | 26 +++++-- .../components/layouts/target-selector.tsx | 69 ++++++++++++++++--- 3 files changed, 79 insertions(+), 25 deletions(-) diff --git a/packages/services/api/src/modules/support/resolvers/SupportTicket.ts b/packages/services/api/src/modules/support/resolvers/SupportTicket.ts index b7c1f1a4fb..1cd012f17a 100644 --- a/packages/services/api/src/modules/support/resolvers/SupportTicket.ts +++ b/packages/services/api/src/modules/support/resolvers/SupportTicket.ts @@ -3,15 +3,6 @@ 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 { diff --git a/packages/web/app/src/components/layouts/project-selector.tsx b/packages/web/app/src/components/layouts/project-selector.tsx index 8110f28670..42232957b8 100644 --- a/packages/web/app/src/components/layouts/project-selector.tsx +++ b/packages/web/app/src/components/layouts/project-selector.tsx @@ -1,7 +1,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger } from '@/components/ui/select'; import { FragmentType, graphql, useFragment } from '@/gql'; import { SelectValue } from '@radix-ui/react-select'; -import { Link } from '@tanstack/react-router'; +import { Link, useRouter } from '@tanstack/react-router'; const ProjectSelector_OrganizationConnectionFragment = graphql(` fragment ProjectSelector_OrganizationConnectionFragment on OrganizationConnection { @@ -24,16 +24,25 @@ export function ProjectSelector(props: { currentOrganizationSlug: string; currentProjectSlug: string; organizations: FragmentType | null; - onValueChange?: Function; + onValueChange?: ((value: string) => void) | undefined; optional?: boolean; showOrganization?: boolean; }) { + const router = useRouter(); const optional = typeof props.optional !== 'undefined' ? props.optional : false; const showOrganization = typeof props.showOrganization !== 'undefined' ? 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 : undefined; - + typeof props.onValueChange !== 'undefined' ? props.onValueChange : defaultFunc; const organizations = useFragment( ProjectSelector_OrganizationConnectionFragment, props.organizations, @@ -65,7 +74,7 @@ export function ProjectSelector(props: { ) : ( '' )} - {projectEdges?.length && currentProject ? ( + {(projectEdges?.length && currentProject) || optional ? ( <> {showOrganization ?
/
: <>} + +
+ {isOptional ? ( + + ) : ( + (currentTarget?.slug ?? '') + )} +
+
+ + {isOptional ? ( + + Unassigned + + ) : ( + <> + )} + {targetEdges ? ( + targetEdges.map(edge => ( + + {edge.node.slug} + + )) + ) : ( + <> + )} + + + ) : (
)} From f979a00dd65d5190e9cbd7711ebef62b5b104a07 Mon Sep 17 00:00:00 2001 From: Emily Goodwin Date: Mon, 23 Jun 2025 20:04:07 -0400 Subject: [PATCH 10/12] fix typecheck --- packages/web/app/src/components/layouts/project.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/web/app/src/components/layouts/project.tsx b/packages/web/app/src/components/layouts/project.tsx index b30b4b25d9..a00eadd6d7 100644 --- a/packages/web/app/src/components/layouts/project.tsx +++ b/packages/web/app/src/components/layouts/project.tsx @@ -15,7 +15,7 @@ import { Form, FormControl, FormField, FormItem, FormMessage } from '@/component import { Input } from '@/components/ui/input'; import { useToast } from '@/components/ui/use-toast'; import { UserMenu } from '@/components/ui/user-menu'; -import { graphql } from '@/gql'; +import { FragmentType, graphql } from '@/gql'; import { useToggle } from '@/lib/hooks'; import { useLastVisitedOrganizationWriter } from '@/lib/last-visited-org'; import { zodResolver } from '@hookform/resolvers/zod'; @@ -96,9 +96,7 @@ export function ProjectLayout({ currentOrganizationSlug={props.organizationSlug} currentProjectSlug={props.projectSlug} organizations={ - (query.data?.organizations as FragmentType< - typeof ProjectSelector_OrganizationConnectionFragment - >) ?? null + (query.data?.organizations) ?? null } />
From 539a7d2f1e19aa2582cf82eb85eb7518df41286f Mon Sep 17 00:00:00 2001 From: Emily Goodwin Date: Mon, 23 Jun 2025 20:44:27 -0400 Subject: [PATCH 11/12] fix lint issue --- packages/web/app/src/components/layouts/project.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/app/src/components/layouts/project.tsx b/packages/web/app/src/components/layouts/project.tsx index a00eadd6d7..692b76ff51 100644 --- a/packages/web/app/src/components/layouts/project.tsx +++ b/packages/web/app/src/components/layouts/project.tsx @@ -15,7 +15,7 @@ import { Form, FormControl, FormField, FormItem, FormMessage } from '@/component import { Input } from '@/components/ui/input'; import { useToast } from '@/components/ui/use-toast'; import { UserMenu } from '@/components/ui/user-menu'; -import { FragmentType, graphql } from '@/gql'; +import { graphql } from '@/gql'; import { useToggle } from '@/lib/hooks'; import { useLastVisitedOrganizationWriter } from '@/lib/last-visited-org'; import { zodResolver } from '@hookform/resolvers/zod'; From 63b812a2f63e4129bc0ea9569868fdd4a4c88a5e Mon Sep 17 00:00:00 2001 From: Emily Goodwin Date: Mon, 23 Jun 2025 21:10:24 -0400 Subject: [PATCH 12/12] fix prettier --- packages/web/app/src/components/layouts/project.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/web/app/src/components/layouts/project.tsx b/packages/web/app/src/components/layouts/project.tsx index 692b76ff51..53754a339c 100644 --- a/packages/web/app/src/components/layouts/project.tsx +++ b/packages/web/app/src/components/layouts/project.tsx @@ -95,9 +95,7 @@ export function ProjectLayout({