import { isUndefined } from 'lodash' import { useRouter } from 'next/router' import { useEffect, useReducer, useState, FC, ChangeEvent, useRef } from 'react' import { observer } from 'mobx-react-lite' import { Button, IconMail, IconPlus, IconX, Input, Listbox } from '@supabase/ui' import { useStore } from 'hooks' import { Project } from 'types' import { post, get } from 'lib/common/fetch' import { API_URL, PRICING_TIER_PRODUCT_IDS } from 'lib/constants' import Divider from 'components/ui/Divider' import Connecting from 'components/ui/Loading/Loading' import { formatMessage, formReducer, uploadAttachments } from './SupportForm.utils' import { DEFAULT_VALUES, CATEGORY_OPTIONS, SEVERITY_OPTIONS, } from 'components/interfaces/Support/Support.constants' const MAX_ATTACHMENTS = 5 interface Props { setSent: (value: boolean) => void } const SupportForm: FC = ({ setSent }) => { const { ui, app } = useStore() const router = useRouter() const projectRef = router.query.ref const category = router.query.category const uploadButtonRef = useRef() const [loading, setLoading] = useState(false) const [formState, formDispatch] = useReducer(formReducer, DEFAULT_VALUES) const [uploadedFiles, setUploadedFiles] = useState([]) const [uploadedDataUrls, setUploadedDataUrls] = useState([]) // Get all orgs and projects from global store const sortedOrganizations = app.organizations.list() const sortedProjects = app.projects.list() const projectDefaults: Partial[] = [{ ref: 'no-project', name: 'No specific project' }] const isInitialized = app.projects.isInitialized const projects = [...sortedProjects, ...projectDefaults] useEffect(() => { if (isInitialized) { // set project default if (sortedProjects.length > 1) { const selectedProject = sortedProjects.find( (project: Project) => project.ref === projectRef ) if (!isUndefined(selectedProject)) { handleOnChange({ name: 'project', value: selectedProject.ref }) } else { handleOnChange({ name: 'project', value: sortedProjects[0].ref }) } } else { // set as 'No specific project' handleOnChange({ name: 'project', value: projectDefaults[0].ref }) } // Set category based on query param if (category) { const selectedCategory = CATEGORY_OPTIONS.find((option) => { if (option.value.toLowerCase() === category) return option }) if (selectedCategory) handleOnChange({ name: 'category', value: selectedCategory.value }) } } }, [isInitialized]) useEffect(() => { if (!uploadedFiles) return const objectUrls = uploadedFiles.map((file) => URL.createObjectURL(file)) setUploadedDataUrls(objectUrls) return () => { objectUrls.forEach((url: any) => URL.revokeObjectURL(url)) } }, [uploadedFiles]) function handleOnChange(x: any) { formDispatch({ name: x.name, value: x.value, error: x.error, }) // Reset severity value when changing project to prevent selection of Critical if (x.name === 'project') { const selectedProject = projects.find((project: any) => project.ref === x.value) if ( (selectedProject?.subscription_tier ?? PRICING_TIER_PRODUCT_IDS.FREE) === PRICING_TIER_PRODUCT_IDS.FREE && formState.severity.value === 'Critical' ) { formDispatch({ name: 'severity', value: 'Low', error: '', }) } } } async function handleSubmit(e: any) { e.preventDefault() let errors: any = [] if (!formState.subject.value) { const message = 'Please add a subject heading' handleOnChange({ name: 'subject', error: message }) errors.push([...errors, message]) } if (!formState.body.value) { const message = 'Please type in a message' handleOnChange({ name: 'body', error: message }) errors.push([...errors, message]) } if (errors.length === 0) { setLoading(true) const projectRef = formState.project.value const attachments = uploadedFiles ? await uploadAttachments(projectRef, uploadedFiles) : [] const payload = { projectRef, message: formatMessage(formState.body.value, attachments), category: formState.category.value, verified: true, tags: ['dashboard-support-form'], subject: formState.subject.value, severity: formState.severity.value, siteUrl: '', additionalRedirectUrls: '', } if (projectRef !== 'no-project') { const URL = `${API_URL}/auth/${projectRef}/config` const authConfig = await get(URL) if (!authConfig.error) { payload.siteUrl = authConfig.SITE_URL payload.additionalRedirectUrls = authConfig.URI_ALLOW_LIST } } const response = await post(`${API_URL}/feedback/send`, payload) setLoading(false) if (response.error) { ui.setNotification({ category: 'error', message: `Failed to submit support ticket: ${response.error.message}`, }) } else { ui.setNotification({ category: 'success', message: 'Support request sent. Thank you!' }) setSent(true) } } } const onFilesUpload = async (event: ChangeEvent) => { event.persist() const items = event.target.files || (event as any).dataTransfer.items const itemsCopied = Array.prototype.map.call(items, (item) => item) as File[] const itemsToBeUploaded = itemsCopied.slice(0, MAX_ATTACHMENTS - uploadedFiles.length) setUploadedFiles(uploadedFiles.concat(itemsToBeUploaded)) if (items.length + uploadedFiles.length > MAX_ATTACHMENTS) { ui.setNotification({ category: 'info', message: `Only up to ${MAX_ATTACHMENTS} attachments are allowed`, }) } event.target.value = '' } const removeUploadedFile = (idx: number) => { const updatedFiles = uploadedFiles?.slice() updatedFiles.splice(idx, 1) setUploadedFiles(updatedFiles) const updatedDataUrls = uploadedDataUrls.slice() uploadedDataUrls.splice(idx, 1) setUploadedDataUrls(updatedDataUrls) } if (!isInitialized) { return (
) } return (
handleSubmit(e)} className="space-y-8">

What problem are you facing?

handleOnChange({ name: 'category', value })} > {CATEGORY_OPTIONS.map((option, i) => { return ( { return ( <> {option.label} {option.description} ) }} /> ) })}
handleOnChange({ name: 'project', value })} > {projects.map((option) => { return ( { const organization = sortedOrganizations.find( (x) => x.id === option.organization_id ) return (
{option.name} {organization?.name}
) }} /> ) })}
handleOnChange({ name: 'severity', value })} > {SEVERITY_OPTIONS.map((option: any) => { const selectedProject = projects.find( (project: any) => project.ref === formState.project.value ) const isAllowedCritical = (selectedProject?.subscription_tier ?? PRICING_TIER_PRODUCT_IDS.FREE) !== PRICING_TIER_PRODUCT_IDS.FREE return ( { return ( <> {option.label} {option.description} ) }} /> ) })}
handleOnChange({ name: 'subject', value: e.target.value })} value={formState.subject.value} error={formState.subject.error} />
handleOnChange({ name: 'body', value: e.target.value })} value={formState.body.value} error={formState.body.error} />

Attachments

Upload up to {MAX_ATTACHMENTS} screenshots that might be relevant to the issue that you're facing

{uploadedDataUrls.map((x: any, idx: number) => (
removeUploadedFile(idx)} >
))} {uploadedFiles.length < MAX_ATTACHMENTS && (
{ if (uploadButtonRef.current) (uploadButtonRef.current as any).click() }} >
)}
) } export default observer(SupportForm)