Remove useProjectsQuery from support form (#39026)
* Remove useProjectsQuery from support form * Add comment for refactor
This commit is contained in:
@@ -2,6 +2,7 @@ import { Check, ExternalLink, Mail, Search } from 'lucide-react'
|
|||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
import { useProjectDetailQuery } from 'data/projects/project-detail-query'
|
||||||
import { useProfile } from 'lib/profile'
|
import { useProfile } from 'lib/profile'
|
||||||
import { Button, Input, Separator } from 'ui'
|
import { Button, Input, Separator } from 'ui'
|
||||||
import { CATEGORY_OPTIONS } from './Support.constants'
|
import { CATEGORY_OPTIONS } from './Support.constants'
|
||||||
@@ -9,18 +10,16 @@ import { CATEGORY_OPTIONS } from './Support.constants'
|
|||||||
interface SuccessProps {
|
interface SuccessProps {
|
||||||
sentCategory?: string
|
sentCategory?: string
|
||||||
selectedProject?: string
|
selectedProject?: string
|
||||||
projects?: any[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Success = ({
|
export const Success = ({ sentCategory = '', selectedProject = 'no-project' }: SuccessProps) => {
|
||||||
sentCategory = '',
|
|
||||||
selectedProject = 'no-project',
|
|
||||||
projects = [],
|
|
||||||
}: SuccessProps) => {
|
|
||||||
const { profile } = useProfile()
|
const { profile } = useProfile()
|
||||||
const respondToEmail = profile?.primary_email ?? 'your email'
|
const respondToEmail = profile?.primary_email ?? 'your email'
|
||||||
|
|
||||||
const project = projects.find((p) => p.ref === selectedProject)
|
const { data: project } = useProjectDetailQuery(
|
||||||
|
{ ref: selectedProject },
|
||||||
|
{ enabled: selectedProject !== 'no-project' }
|
||||||
|
)
|
||||||
const projectName = project ? project.name : 'No specific project'
|
const projectName = project ? project.name : 'No specific project'
|
||||||
|
|
||||||
const categoriesToShowAdditionalResources = ['Problem', 'Unresponsive', 'Performance']
|
const categoriesToShowAdditionalResources = ['Problem', 'Unresponsive', 'Performance']
|
||||||
@@ -98,5 +97,3 @@ const Success = ({
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Success
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { zodResolver } from '@hookform/resolvers/zod'
|
import { zodResolver } from '@hookform/resolvers/zod'
|
||||||
import * as Sentry from '@sentry/nextjs'
|
import * as Sentry from '@sentry/nextjs'
|
||||||
|
import { AnimatePresence, motion } from 'framer-motion'
|
||||||
import {
|
import {
|
||||||
Book,
|
Book,
|
||||||
Check,
|
Check,
|
||||||
@@ -13,6 +14,7 @@ import {
|
|||||||
X,
|
X,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
import { ChangeEvent, useEffect, useMemo, useRef, useState } from 'react'
|
import { ChangeEvent, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { SubmitHandler, useForm } from 'react-hook-form'
|
import { SubmitHandler, useForm } from 'react-hook-form'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
@@ -20,15 +22,16 @@ import * as z from 'zod'
|
|||||||
|
|
||||||
import { useDocsSearch, useParams, type DocsSearchResult as Page } from 'common'
|
import { useDocsSearch, useParams, type DocsSearchResult as Page } from 'common'
|
||||||
import { CLIENT_LIBRARIES } from 'common/constants'
|
import { CLIENT_LIBRARIES } from 'common/constants'
|
||||||
|
import CopyButton from 'components/ui/CopyButton'
|
||||||
import { OrganizationProjectSelector } from 'components/ui/OrganizationProjectSelector'
|
import { OrganizationProjectSelector } from 'components/ui/OrganizationProjectSelector'
|
||||||
import { getProjectAuthConfig } from 'data/auth/auth-config-query'
|
import { getProjectAuthConfig } from 'data/auth/auth-config-query'
|
||||||
import { useSendSupportTicketMutation } from 'data/feedback/support-ticket-send'
|
import { useSendSupportTicketMutation } from 'data/feedback/support-ticket-send'
|
||||||
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
|
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
|
||||||
import { useProjectsQuery } from 'data/projects/projects-query'
|
import { getProjectDetail } from 'data/projects/project-detail-query'
|
||||||
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
|
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
|
||||||
import { detectBrowser } from 'lib/helpers'
|
import { detectBrowser } from 'lib/helpers'
|
||||||
import { useProfile } from 'lib/profile'
|
import { useProfile } from 'lib/profile'
|
||||||
import { useRouter } from 'next/router'
|
import { useQueryState } from 'nuqs'
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
@@ -72,6 +75,41 @@ const MAX_ATTACHMENTS = 5
|
|||||||
const INCLUDE_DISCUSSIONS = ['Problem', 'Database_unresponsive']
|
const INCLUDE_DISCUSSIONS = ['Problem', 'Database_unresponsive']
|
||||||
const CONTAINER_CLASSES = 'px-6'
|
const CONTAINER_CLASSES = 'px-6'
|
||||||
|
|
||||||
|
const FormSchema = z
|
||||||
|
.object({
|
||||||
|
organizationSlug: z.string().min(1, 'Please select an organization'),
|
||||||
|
projectRef: z.string().min(1, 'Please select a project'),
|
||||||
|
category: z.string().min(1, 'Please select an issue type'),
|
||||||
|
severity: z.string(),
|
||||||
|
library: z.string(),
|
||||||
|
subject: z.string().min(1, 'Please add a subject heading'),
|
||||||
|
message: z.string().min(1, "Please add a message about the issue that you're facing"),
|
||||||
|
affectedServices: z.string(),
|
||||||
|
allowSupportAccess: z.boolean(),
|
||||||
|
})
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
return !(data.category === 'Problem' && data.library === '')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "Please select the library that you're facing issues with",
|
||||||
|
path: ['library'],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultValues = {
|
||||||
|
organizationSlug: '',
|
||||||
|
// [Joshen TODO] We should refactor this to accept a null value instead of a magic string
|
||||||
|
projectRef: 'no-project',
|
||||||
|
category: '',
|
||||||
|
severity: 'Low',
|
||||||
|
library: '',
|
||||||
|
subject: '',
|
||||||
|
message: '',
|
||||||
|
affectedServices: '',
|
||||||
|
allowSupportAccess: true,
|
||||||
|
}
|
||||||
|
|
||||||
interface SupportFormV2Props {
|
interface SupportFormV2Props {
|
||||||
onProjectSelected: (value: string) => void
|
onProjectSelected: (value: string) => void
|
||||||
onOrganizationSelected: (value: string) => void
|
onOrganizationSelected: (value: string) => void
|
||||||
@@ -86,9 +124,12 @@ export const SupportFormV2 = ({
|
|||||||
setSentCategory,
|
setSentCategory,
|
||||||
}: SupportFormV2Props) => {
|
}: SupportFormV2Props) => {
|
||||||
const { profile } = useProfile()
|
const { profile } = useProfile()
|
||||||
|
const [highlightRef, setHighlightRef] = useQueryState('highlight', { defaultValue: '' })
|
||||||
|
|
||||||
|
// [Joshen] Ideally refactor all these to use nuqs
|
||||||
const {
|
const {
|
||||||
projectRef: ref,
|
projectRef: urlRef,
|
||||||
slug,
|
slug: urlSlug,
|
||||||
category: urlCategory,
|
category: urlCategory,
|
||||||
subject: urlSubject,
|
subject: urlSubject,
|
||||||
message: urlMessage,
|
message: urlMessage,
|
||||||
@@ -103,40 +144,6 @@ export const SupportFormV2 = ({
|
|||||||
const [uploadedFiles, setUploadedFiles] = useState<File[]>([])
|
const [uploadedFiles, setUploadedFiles] = useState<File[]>([])
|
||||||
const [uploadedDataUrls, setUploadedDataUrls] = useState<string[]>([])
|
const [uploadedDataUrls, setUploadedDataUrls] = useState<string[]>([])
|
||||||
|
|
||||||
const FormSchema = z
|
|
||||||
.object({
|
|
||||||
organizationSlug: z.string().min(1, 'Please select an organization'),
|
|
||||||
projectRef: z.string().min(1, 'Please select a project'),
|
|
||||||
category: z.string().min(1, 'Please select an issue type'),
|
|
||||||
severity: z.string(),
|
|
||||||
library: z.string(),
|
|
||||||
subject: z.string().min(1, 'Please add a subject heading'),
|
|
||||||
message: z.string().min(1, "Please add a message about the issue that you're facing"),
|
|
||||||
affectedServices: z.string(),
|
|
||||||
allowSupportAccess: z.boolean(),
|
|
||||||
})
|
|
||||||
.refine(
|
|
||||||
(data) => {
|
|
||||||
return !(data.category === 'Problem' && data.library === '')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: "Please select the library that you're facing issues with",
|
|
||||||
path: ['library'],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const defaultValues = {
|
|
||||||
organizationSlug: '',
|
|
||||||
projectRef: 'no-project',
|
|
||||||
category: '',
|
|
||||||
severity: 'Low',
|
|
||||||
library: '',
|
|
||||||
subject: '',
|
|
||||||
message: '',
|
|
||||||
affectedServices: '',
|
|
||||||
allowSupportAccess: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
const form = useForm<z.infer<typeof FormSchema>>({
|
const form = useForm<z.infer<typeof FormSchema>>({
|
||||||
mode: 'onBlur',
|
mode: 'onBlur',
|
||||||
reValidateMode: 'onBlur',
|
reValidateMode: 'onBlur',
|
||||||
@@ -158,8 +165,6 @@ export const SupportFormV2 = ({
|
|||||||
() => organizations?.find((org) => org.slug === organizationSlug),
|
() => organizations?.find((org) => org.slug === organizationSlug),
|
||||||
[organizationSlug, organizations]
|
[organizationSlug, organizations]
|
||||||
)
|
)
|
||||||
const { data, isSuccess: isSuccessProjects } = useProjectsQuery()
|
|
||||||
const allProjects = data?.projects ?? []
|
|
||||||
|
|
||||||
const { mutate: sendEvent } = useSendEventMutation()
|
const { mutate: sendEvent } = useSendEventMutation()
|
||||||
|
|
||||||
@@ -282,28 +287,32 @@ export const SupportFormV2 = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// For prefilling form fields via URL, project ref will taking higher precedence than org slug
|
// For prefilling form fields via URL, project ref will taking higher precedence than org slug
|
||||||
if (isSuccessOrganizations && isSuccessProjects) {
|
const prefillForm = async () => {
|
||||||
if (organizations.length === 0) {
|
if (isSuccessOrganizations) {
|
||||||
form.setValue('organizationSlug', 'no-org')
|
if (organizations.length === 0) {
|
||||||
} else if (ref) {
|
form.setValue('organizationSlug', 'no-org')
|
||||||
const selectedProject = allProjects.find((p) => p.ref === ref)
|
} else if (urlRef) {
|
||||||
if (selectedProject !== undefined) {
|
// Check validity of project via project details
|
||||||
form.setValue('organizationSlug', selectedProject.organization_slug)
|
const selectedProject = await getProjectDetail({ ref: urlRef })
|
||||||
form.setValue('projectRef', selectedProject.ref)
|
if (!!selectedProject) {
|
||||||
}
|
const org = organizations.find((x) => x.id === selectedProject.organization_id)
|
||||||
} else if (slug) {
|
if (!!org) form.setValue('organizationSlug', org.slug)
|
||||||
if (organizations.some((it) => it.slug === slug)) {
|
form.setValue('projectRef', selectedProject.ref)
|
||||||
form.setValue('organizationSlug', slug)
|
}
|
||||||
}
|
} else if (urlSlug) {
|
||||||
} else if (ref === undefined && slug === undefined) {
|
if (organizations.some((it) => it.slug === urlSlug)) {
|
||||||
const firstOrganization = organizations?.[0]
|
form.setValue('organizationSlug', urlSlug)
|
||||||
if (firstOrganization !== undefined) {
|
}
|
||||||
form.setValue('organizationSlug', firstOrganization.slug)
|
} else if (!urlRef && !urlSlug) {
|
||||||
|
const firstOrganization = organizations?.[0]
|
||||||
|
if (!!firstOrganization) {
|
||||||
|
form.setValue('organizationSlug', firstOrganization.slug)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
prefillForm()
|
||||||
}, [ref, slug, isSuccessOrganizations, isSuccessProjects])
|
}, [urlRef, urlSlug, isSuccessOrganizations])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (urlCategory) {
|
if (urlCategory) {
|
||||||
@@ -396,19 +405,21 @@ export const SupportFormV2 = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={cn(CONTAINER_CLASSES, 'flex flex-col gap-y-2')}>
|
<div id="projectRef-field" className={cn(CONTAINER_CLASSES, 'flex flex-col gap-y-2')}>
|
||||||
<FormField_Shadcn_
|
<FormField_Shadcn_
|
||||||
name="projectRef"
|
name="projectRef"
|
||||||
control={form.control}
|
control={form.control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItemLayout layout="vertical" label="Which project is affected?">
|
<FormItemLayout hideMessage layout="vertical" label="Which project is affected?">
|
||||||
<FormControl_Shadcn_>
|
<FormControl_Shadcn_>
|
||||||
<OrganizationProjectSelector
|
<OrganizationProjectSelector
|
||||||
sameWidthAsTrigger
|
sameWidthAsTrigger
|
||||||
checkPosition="left"
|
checkPosition="left"
|
||||||
slug={organizationSlug}
|
slug={organizationSlug}
|
||||||
selectedRef={field.value}
|
selectedRef={field.value}
|
||||||
onInitialLoad={(projects) => field.onChange(projects[0]?.ref ?? 'no-project')}
|
onInitialLoad={(projects) => {
|
||||||
|
if (!urlRef) field.onChange(projects[0]?.ref ?? 'no-project')
|
||||||
|
}}
|
||||||
onSelect={(project) => field.onChange(project.ref)}
|
onSelect={(project) => field.onChange(project.ref)}
|
||||||
renderTrigger={({ isLoading, project }) => (
|
renderTrigger={({ isLoading, project }) => (
|
||||||
<Button
|
<Button
|
||||||
@@ -450,6 +461,46 @@ export const SupportFormV2 = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<AnimatePresence>
|
||||||
|
{projectRef !== 'no-project' && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, height: 0 }}
|
||||||
|
animate={{ opacity: 1, height: 'auto' }}
|
||||||
|
exit={{ opacity: 0, height: 0 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
className="flex items-center gap-x-1"
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
className={cn(
|
||||||
|
'text-sm prose transition',
|
||||||
|
highlightRef ? 'text-foreground' : 'text-foreground-lighter'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Project ID:{' '}
|
||||||
|
<code
|
||||||
|
className={cn(
|
||||||
|
'transition',
|
||||||
|
highlightRef
|
||||||
|
? 'text-brand font-medium border-brand-500 animate-pulse'
|
||||||
|
: 'text-foreground-light'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{projectRef}
|
||||||
|
</code>
|
||||||
|
</p>
|
||||||
|
<CopyButton
|
||||||
|
iconOnly
|
||||||
|
type="text"
|
||||||
|
text={projectRef}
|
||||||
|
onClick={() => {
|
||||||
|
toast.success('Copied to clipboard')
|
||||||
|
setHighlightRef(null)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
{organizationSlug &&
|
{organizationSlug &&
|
||||||
subscriptionPlanId !== 'enterprise' &&
|
subscriptionPlanId !== 'enterprise' &&
|
||||||
category !== 'Login_issues' && (
|
category !== 'Login_issues' && (
|
||||||
|
|||||||
@@ -1,22 +1,23 @@
|
|||||||
import { ClipboardIcon, Loader2, Wrench } from 'lucide-react'
|
import { Loader2, Wrench } from 'lucide-react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import SVG from 'react-inlinesvg'
|
import SVG from 'react-inlinesvg'
|
||||||
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
import { AIAssistantOption } from 'components/interfaces/Support/AIAssistantOption'
|
import { AIAssistantOption } from 'components/interfaces/Support/AIAssistantOption'
|
||||||
import Success from 'components/interfaces/Support/Success'
|
import { Success } from 'components/interfaces/Support/Success'
|
||||||
import { SupportFormV2 } from 'components/interfaces/Support/SupportFormV2'
|
import { SupportFormV2 } from 'components/interfaces/Support/SupportFormV2'
|
||||||
import AppLayout from 'components/layouts/AppLayout/AppLayout'
|
import AppLayout from 'components/layouts/AppLayout/AppLayout'
|
||||||
import DefaultLayout from 'components/layouts/DefaultLayout'
|
import DefaultLayout from 'components/layouts/DefaultLayout'
|
||||||
import CopyButton from 'components/ui/CopyButton'
|
import CopyButton from 'components/ui/CopyButton'
|
||||||
import InformationBox from 'components/ui/InformationBox'
|
import InformationBox from 'components/ui/InformationBox'
|
||||||
|
import { InlineLink, InlineLinkClassName } from 'components/ui/InlineLink'
|
||||||
import { usePlatformStatusQuery } from 'data/platform/platform-status-query'
|
import { usePlatformStatusQuery } from 'data/platform/platform-status-query'
|
||||||
import { useProjectsQuery } from 'data/projects/projects-query'
|
|
||||||
import { withAuth } from 'hooks/misc/withAuth'
|
import { withAuth } from 'hooks/misc/withAuth'
|
||||||
import { BASE_PATH } from 'lib/constants'
|
import { BASE_PATH } from 'lib/constants'
|
||||||
import { toast } from 'sonner'
|
import { useQueryState } from 'nuqs'
|
||||||
import { NextPageWithLayout } from 'types'
|
import { NextPageWithLayout } from 'types'
|
||||||
import { Button, copyToClipboard, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
|
import { Button, cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
|
||||||
|
|
||||||
const SupportPage: NextPageWithLayout = () => {
|
const SupportPage: NextPageWithLayout = () => {
|
||||||
const [sentCategory, setSentCategory] = useState<string>()
|
const [sentCategory, setSentCategory] = useState<string>()
|
||||||
@@ -27,7 +28,7 @@ const SupportPage: NextPageWithLayout = () => {
|
|||||||
const { data, isLoading } = usePlatformStatusQuery()
|
const { data, isLoading } = usePlatformStatusQuery()
|
||||||
const isHealthy = data?.isHealthy
|
const isHealthy = data?.isHealthy
|
||||||
|
|
||||||
const { data: projectsData } = useProjectsQuery()
|
const [_, setHighlightRef] = useQueryState('highlight', { defaultValue: '' })
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex overflow-y-auto overflow-x-hidden">
|
<div className="relative flex overflow-y-auto overflow-x-hidden">
|
||||||
@@ -88,11 +89,7 @@ const SupportPage: NextPageWithLayout = () => {
|
|||||||
].join(' ')}
|
].join(' ')}
|
||||||
>
|
>
|
||||||
{sentCategory !== undefined ? (
|
{sentCategory !== undefined ? (
|
||||||
<Success
|
<Success sentCategory={sentCategory} selectedProject={selectedProject} />
|
||||||
sentCategory={sentCategory}
|
|
||||||
selectedProject={selectedProject}
|
|
||||||
projects={projectsData?.projects}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<SupportFormV2
|
<SupportFormV2
|
||||||
onProjectSelected={setSelectedProject}
|
onProjectSelected={setSelectedProject}
|
||||||
@@ -106,14 +103,11 @@ const SupportPage: NextPageWithLayout = () => {
|
|||||||
title="Having trouble submitting the form?"
|
title="Having trouble submitting the form?"
|
||||||
description={
|
description={
|
||||||
<div className="flex flex-col gap-y-4">
|
<div className="flex flex-col gap-y-4">
|
||||||
<p className="flex items-center gap-x-1">
|
<p className="flex items-center gap-x-1 ">
|
||||||
Email us directly at{' '}
|
Email us directly at{' '}
|
||||||
<Link
|
<InlineLink href="mailto:support@supabase.com" className="font-mono">
|
||||||
href="mailto:support@supabase.com"
|
|
||||||
className="p-1 font-mono rounded-md text-foreground"
|
|
||||||
>
|
|
||||||
support@supabase.com
|
support@supabase.com
|
||||||
</Link>
|
</InlineLink>
|
||||||
<CopyButton
|
<CopyButton
|
||||||
type="text"
|
type="text"
|
||||||
text="support@supabase.com"
|
text="support@supabase.com"
|
||||||
@@ -123,39 +117,16 @@ const SupportPage: NextPageWithLayout = () => {
|
|||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Please, make sure to{' '}
|
Please, make sure to{' '}
|
||||||
<Tooltip>
|
<span
|
||||||
<TooltipTrigger asChild>
|
className={cn(InlineLinkClassName, 'cursor-pointer')}
|
||||||
<span className="text-foreground underline">include your project ID</span>
|
onClick={() => {
|
||||||
</TooltipTrigger>
|
const el = document.getElementById('projectRef-field')
|
||||||
<TooltipContent className="px-0">
|
el?.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'center' })
|
||||||
<ul className="p-2">
|
setHighlightRef('true')
|
||||||
<li className="grid pb-1 grid-cols-2 px-2 text-foreground-lighter">
|
}}
|
||||||
<span>Project name</span>
|
>
|
||||||
<span>ID</span>
|
include your project ID
|
||||||
</li>
|
</span>{' '}
|
||||||
{(projectsData?.projects ?? []).map((project) => (
|
|
||||||
<li key={project.id} className="cursor-default">
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
copyToClipboard(project.ref)
|
|
||||||
toast.success('Copied to clipboard')
|
|
||||||
}}
|
|
||||||
className="w-full group py-1.5 px-2 gap-x-1 text-foreground hover:bg-muted grid grid-cols-2 text-left rounded-sm"
|
|
||||||
>
|
|
||||||
<span className="truncate max-w-40">{project.name}</span>
|
|
||||||
<span className="flex w-full gap-x-1 items-center font-mono">
|
|
||||||
{project.ref}
|
|
||||||
<ClipboardIcon
|
|
||||||
size="14"
|
|
||||||
className="text-foreground-lighter opacity-0 group-hover:opacity-100 transition-opacity"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>{' '}
|
|
||||||
and as much information as possible.
|
and as much information as possible.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user