mirror of
https://github.com/supabase/supabase.git
synced 2026-10-09 03:15:06 +03:00
Fixes FE-3801. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Updated the project deletion confirmation dialog so the “learn more / documentation” text and external link are always shown consistently. * Paid projects now include the full warning (“All project data will be lost, and cannot be undone.”) alongside the documentation link, while free projects show the simplified message without the additional warning. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
212 lines
7.6 KiB
TypeScript
212 lines
7.6 KiB
TypeScript
import { useRouter } from 'next/router'
|
|
import { useEffect, useState } from 'react'
|
|
import { toast } from 'sonner'
|
|
import { TextArea } from 'ui'
|
|
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
|
|
|
import { CANCELLATION_REASONS } from '@/components/interfaces/Billing/Billing.constants'
|
|
import { LogicalBackupCliInstructions } from '@/components/layouts/ProjectLayout/LogicalBackupCliInstructions'
|
|
import { TextConfirmModal } from '@/components/ui/TextConfirmModalWrapper'
|
|
import { useSendDowngradeFeedbackMutation } from '@/data/feedback/exit-survey-send'
|
|
import type { OrgProject } from '@/data/projects/org-projects-infinite-query'
|
|
import { useProjectDeleteMutation } from '@/data/projects/project-delete-mutation'
|
|
import { useOrgSubscriptionQuery } from '@/data/subscriptions/org-subscription-query'
|
|
import { useLastVisitedOrganization } from '@/hooks/misc/useLastVisitedOrganization'
|
|
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
|
|
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
|
import type { Organization } from '@/types'
|
|
|
|
export const DeleteProjectModal = ({
|
|
visible,
|
|
onClose,
|
|
project: projectProp,
|
|
organization: organizationProp,
|
|
}: {
|
|
visible: boolean
|
|
onClose: () => void
|
|
project?: OrgProject
|
|
organization?: Organization
|
|
}) => {
|
|
const router = useRouter()
|
|
const { data: projectFromQuery } = useSelectedProjectQuery()
|
|
const { data: organizationFromQuery } = useSelectedOrganizationQuery()
|
|
|
|
// Use props if provided, otherwise fall back to hooks
|
|
const project = projectProp || projectFromQuery
|
|
const organization = organizationProp || organizationFromQuery
|
|
|
|
const { lastVisitedOrganization } = useLastVisitedOrganization()
|
|
|
|
const projectRef = project?.ref
|
|
const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: organization?.slug })
|
|
const projectPlan = subscription?.plan?.id ?? 'free'
|
|
const isFree = projectPlan === 'free'
|
|
|
|
const [message, setMessage] = useState<string>('')
|
|
const [selectedReason, setSelectedReason] = useState<string[]>([])
|
|
|
|
// Single select for cancellation reason
|
|
const onSelectCancellationReason = (reason: string) => {
|
|
setSelectedReason([reason])
|
|
}
|
|
|
|
// Helper to get label for selected reason
|
|
const getReasonLabel = (reason: string | undefined) => {
|
|
const found = CANCELLATION_REASONS.find((r) => r.value === reason)
|
|
return found?.label || 'What can we improve on?'
|
|
}
|
|
|
|
const textareaLabel = getReasonLabel(selectedReason[0])
|
|
|
|
const [shuffledReasons] = useState(() => [
|
|
...CANCELLATION_REASONS.sort(() => Math.random() - 0.5),
|
|
{ value: 'None of the above' },
|
|
])
|
|
|
|
const { mutate: deleteProject, isPending: isDeleting } = useProjectDeleteMutation({
|
|
onSuccess: async () => {
|
|
if (!isFree) {
|
|
try {
|
|
await sendExitSurvey({
|
|
orgSlug: organization?.slug,
|
|
projectRef,
|
|
message,
|
|
reasons: selectedReason.reduce((a, b) => `${a}- ${b}\n`, ''),
|
|
exitAction: 'delete',
|
|
})
|
|
} catch (error) {
|
|
// [Joshen] In this case we don't raise any errors if the exit survey fails to send since it shouldn't block the user
|
|
}
|
|
}
|
|
|
|
toast.success(`Successfully deleted ${project?.name}`)
|
|
|
|
// Only redirect if still viewing the deleted project
|
|
if (router.asPath.startsWith(`/project/${projectRef}`)) {
|
|
if (lastVisitedOrganization) {
|
|
router.push(`/org/${lastVisitedOrganization}`)
|
|
} else {
|
|
router.push('/organizations')
|
|
}
|
|
}
|
|
},
|
|
})
|
|
const { mutateAsync: sendExitSurvey, isPending: isSending } = useSendDowngradeFeedbackMutation()
|
|
const isSubmitting = isDeleting || isSending
|
|
|
|
async function handleDeleteProject() {
|
|
if (project === undefined) return
|
|
if (!isFree && selectedReason.length === 0) {
|
|
return toast.error('Please select a reason for deleting your project')
|
|
}
|
|
|
|
deleteProject({ projectRef: project.ref, organizationSlug: organization?.slug })
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (visible) {
|
|
setSelectedReason([])
|
|
setMessage('')
|
|
}
|
|
}, [visible])
|
|
|
|
return (
|
|
<TextConfirmModal
|
|
visible={visible}
|
|
loading={isSubmitting}
|
|
size={isFree ? 'medium' : 'xlarge'}
|
|
title={`Confirm deletion of ${project?.name}`}
|
|
variant="destructive"
|
|
alert={{
|
|
title: isFree
|
|
? 'This action cannot be undone.'
|
|
: `This will permanently delete the ${project?.name}`,
|
|
description: (
|
|
<>
|
|
{!isFree && 'All project data will be lost, and cannot be undone. '}
|
|
Read the{' '}
|
|
<a
|
|
href="https://supabase.com/docs/guides/platform/delete-project"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="underline hover:text-foreground"
|
|
>
|
|
documentation
|
|
</a>{' '}
|
|
for prerequisites, implications, and recovery information.
|
|
</>
|
|
),
|
|
}}
|
|
text={
|
|
isFree
|
|
? `This will permanently delete the ${project?.name} project and all of its data.`
|
|
: undefined
|
|
}
|
|
confirmPlaceholder="Type the project name in here"
|
|
confirmString={project?.name || ''}
|
|
confirmLabel="I understand, delete this project"
|
|
onConfirm={handleDeleteProject}
|
|
onCancel={() => {
|
|
if (!isSubmitting) onClose()
|
|
}}
|
|
>
|
|
<div className="space-y-6">
|
|
<LogicalBackupCliInstructions enabled={visible} showResetPassword={false} />
|
|
|
|
{/*
|
|
[Joshen] This is basically ExitSurvey.tsx, ideally we have one shared component but the one
|
|
in ExitSurvey has a Form wrapped around it already. Will probably need some effort to refactor
|
|
but leaving that for the future.
|
|
*/}
|
|
{!isFree && (
|
|
<div className="flex flex-col gap-y-6">
|
|
<FormItemLayout
|
|
isReactForm={false}
|
|
label="What made you decide to delete your project?"
|
|
>
|
|
<div className="flex flex-wrap gap-2" data-toggle="buttons">
|
|
{shuffledReasons.map((option) => {
|
|
const active = selectedReason[0] === option.value
|
|
return (
|
|
<label
|
|
key={option.value}
|
|
className={[
|
|
'flex cursor-pointer items-center space-x-2 rounded-md py-1',
|
|
'pl-2 pr-3 text-center text-sm shadow-xs transition-all duration-100',
|
|
`${
|
|
active
|
|
? ` bg-foreground text-background opacity-100 hover:bg-foreground/75`
|
|
: ` bg-border-strong text-foreground opacity-50 hover:opacity-75`
|
|
}`,
|
|
].join(' ')}
|
|
>
|
|
<input
|
|
type="radio"
|
|
name="options"
|
|
value={option.value}
|
|
className="hidden"
|
|
checked={active}
|
|
onChange={() => onSelectCancellationReason(option.value)}
|
|
/>
|
|
<div>{option.value}</div>
|
|
</label>
|
|
)
|
|
})}
|
|
</div>
|
|
</FormItemLayout>
|
|
|
|
<FormItemLayout isReactForm={false} label={textareaLabel}>
|
|
<TextArea
|
|
name="message"
|
|
rows={3}
|
|
value={message}
|
|
onChange={(event) => setMessage(event.target.value)}
|
|
/>
|
|
</FormItemLayout>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</TextConfirmModal>
|
|
)
|
|
}
|