mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
Bring Branching 2.0 out of feature preview (#44279)
## Context Have Branching 2.0 as the default behaviour + remove it from feature preview Behaviour should match staging / prod if branching 2.0 feature preview is toggled on ## To test - [ ] Test branching flow in general for any oddities <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Chores** * Removed the Branching 2.0 preview and cleared its persisted preview setting; branching UI and branch editing are now available without opt‑in. * Simplified branch management flows and empty states by removing preview-dependent conditions and tooltips. * Made GitHub branch sync optional in create/edit forms and simplified validation and submit behavior. * "Create merge request" and related branch actions now render consistently across the UI. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
1 parent
255d754eb4
commit
be0d05fb08
12 files changed
+263
-396
No files matched your search
@@ -1,60 +0,0 @@
|
||||
import Image from 'next/image'
|
||||
|
||||
import { InlineLink } from '@/components/ui/InlineLink'
|
||||
import { BASE_PATH, DOCS_URL } from '@/lib/constants'
|
||||
|
||||
export const Branching2Preview = () => {
|
||||
return (
|
||||
<div>
|
||||
<p className="text-sm text-foreground-light mb-4">
|
||||
Create branches, review changes, and merge back into production all through the dashboard.
|
||||
Read the below limitations and our{' '}
|
||||
<InlineLink href={`${DOCS_URL}/guides/platform/branching`}>
|
||||
branching documentation
|
||||
</InlineLink>{' '}
|
||||
before opting in.
|
||||
</p>
|
||||
|
||||
<Image
|
||||
src={`${BASE_PATH}/img/previews/branching-preview.png`}
|
||||
width={1296}
|
||||
height={900}
|
||||
alt="api-docs-side-panel-preview"
|
||||
className="rounded border mb-4"
|
||||
/>
|
||||
|
||||
<div className="my-6">
|
||||
<p className="text-sm text-foreground mb-2 font-medium">Limitations:</p>
|
||||
<ul className="list-disc pl-6 text-sm text-foreground-light space-y-1">
|
||||
<li>Custom roles created through the dashboard are not captured on branch creation.</li>
|
||||
<li>
|
||||
Branches can only be merged to <code>main</code>; merging between preview branches is
|
||||
not supported.
|
||||
</li>
|
||||
<li>
|
||||
If your branch is out of date, you can pull in latest changes from <code>main</code>,
|
||||
but keep in mind that all functions will be overwritten.
|
||||
</li>
|
||||
<li>
|
||||
Deleting functions must be done manually on <code>main</code>.
|
||||
</li>
|
||||
<li>Migration conflicts must be manually resolved on the preview branch.</li>
|
||||
<li>
|
||||
If you have run migrations on <code>main</code>, new branches will be created from
|
||||
existing migrations instead of a full schema dump.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 !mt-4">
|
||||
<p className="text-sm">Enabling this preview will:</p>
|
||||
<ul className="list-disc pl-6 text-sm text-foreground-light space-y-1">
|
||||
<li>
|
||||
Allow you to create, manage, review, and merge database branches directly via the
|
||||
branches page within the dashboard
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -95,11 +95,6 @@ export const useUnifiedLogsPreview = () => {
|
||||
return { isEnabled, enable, disable }
|
||||
}
|
||||
|
||||
export const useIsBranching2Enabled = () => {
|
||||
const { flags } = useFeaturePreviewContext()
|
||||
return flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_BRANCHING_2_0]
|
||||
}
|
||||
|
||||
export const useIsPgDeltaDiffEnabled = () => {
|
||||
const { flags } = useFeaturePreviewContext()
|
||||
const pgDeltaDiffEnabled = useFlag('pgdeltaDiff')
|
||||
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
} from 'ui/src/components/shadcn/ui/select'
|
||||
|
||||
import { AdvisorRulesPreview } from './AdvisorRulesPreview'
|
||||
import { Branching2Preview } from './Branching2Preview'
|
||||
import { CLSPreview } from './CLSPreview'
|
||||
import { useFeaturePreviewContext, useFeaturePreviewModal } from './FeaturePreviewContext'
|
||||
import { FloatingMobileToolbarPreview } from './FloatingMobileToolbarPreview'
|
||||
@@ -40,7 +39,6 @@ import { IS_PLATFORM } from '@/lib/constants'
|
||||
const FEATURE_PREVIEW_KEY_TO_CONTENT: {
|
||||
[key: string]: ReactNode
|
||||
} = {
|
||||
[LOCAL_STORAGE_KEYS.UI_PREVIEW_BRANCHING_2_0]: <Branching2Preview />,
|
||||
[LOCAL_STORAGE_KEYS.UI_PREVIEW_PG_DELTA_DIFF]: <PgDeltaDiffPreview />,
|
||||
[LOCAL_STORAGE_KEYS.UI_PREVIEW_ADVISOR_RULES]: <AdvisorRulesPreview />,
|
||||
[LOCAL_STORAGE_KEYS.UI_PREVIEW_CLS]: <CLSPreview />,
|
||||
|
||||
@@ -31,15 +31,6 @@ export const useFeaturePreviews = (): FeaturePreview[] => {
|
||||
isPlatformOnly: true,
|
||||
isDefaultOptIn: false,
|
||||
},
|
||||
{
|
||||
key: LOCAL_STORAGE_KEYS.UI_PREVIEW_BRANCHING_2_0,
|
||||
name: 'Branching via dashboard',
|
||||
discussionsUrl: 'https://github.com/orgs/supabase/discussions/branching-2-0',
|
||||
enabled: true,
|
||||
isNew: false,
|
||||
isPlatformOnly: true,
|
||||
isDefaultOptIn: false,
|
||||
},
|
||||
{
|
||||
key: LOCAL_STORAGE_KEYS.UI_PREVIEW_ADVISOR_RULES,
|
||||
name: 'Disable Advisor rules',
|
||||
|
||||
@@ -22,6 +22,7 @@ interface BranchSelectorProps {
|
||||
disabled?: boolean
|
||||
isUpdating?: boolean
|
||||
type?: 'primary' | 'outline'
|
||||
align?: 'end' | 'center'
|
||||
}
|
||||
|
||||
export const BranchSelector = ({
|
||||
@@ -30,14 +31,12 @@ export const BranchSelector = ({
|
||||
disabled = false,
|
||||
isUpdating = false,
|
||||
type = 'primary',
|
||||
align = 'end',
|
||||
}: BranchSelectorProps) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [selectedBranch, setSelectedBranch] = useState<Branch | null>(null)
|
||||
|
||||
// Filter out branches that are already ready for review or linked to a github branch
|
||||
const availableBranches = branches.filter(
|
||||
(branch) => !branch.is_default && !branch.review_requested_at && !branch.git_branch
|
||||
)
|
||||
const availableBranches = branches.filter((branch) => !branch.is_default)
|
||||
|
||||
const handleBranchSelect = (branch: Branch) => {
|
||||
setSelectedBranch(branch)
|
||||
@@ -51,16 +50,17 @@ export const BranchSelector = ({
|
||||
<ButtonTooltip
|
||||
icon={<GitMerge size={14} strokeWidth={1.5} />}
|
||||
type={type}
|
||||
disabled={disabled || availableBranches.length === 0 || isUpdating}
|
||||
disabled={
|
||||
disabled || isUpdating || branches.length === 0 || availableBranches.length === 0
|
||||
}
|
||||
tooltip={{
|
||||
content: {
|
||||
side: 'bottom',
|
||||
align,
|
||||
text:
|
||||
branches.length === 0
|
||||
branches.length === 0 || availableBranches.length === 0
|
||||
? 'Create a branch first to start a merge request'
|
||||
: availableBranches.length === 0
|
||||
? 'All branches currently have merge requests'
|
||||
: undefined,
|
||||
: undefined,
|
||||
},
|
||||
}}
|
||||
>
|
||||
@@ -80,7 +80,7 @@ export const BranchSelector = ({
|
||||
value={branch.name.replaceAll('"', '')}
|
||||
className="cursor-pointer w-full flex items-center justify-between"
|
||||
onSelect={() => handleBranchSelect(branch)}
|
||||
disabled={isUpdating}
|
||||
disabled={isUpdating || !!branch.git_branch || !!branch.review_requested_at}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{branch.is_default && <Shield size={14} className="text-amber-900" />}
|
||||
@@ -91,6 +91,8 @@ export const BranchSelector = ({
|
||||
{selectedBranch?.id === branch.id && (
|
||||
<Check size={14} strokeWidth={1.5} className="text-brand" />
|
||||
)}
|
||||
{branch.git_branch && <span>Synced to a Git branch</span>}
|
||||
{branch.review_requested_at && <span>Merge request opened</span>}
|
||||
</CommandItem_Shadcn_>
|
||||
))}
|
||||
</ScrollArea>
|
||||
|
||||
@@ -32,7 +32,7 @@ import {
|
||||
TooltipTrigger,
|
||||
} from 'ui'
|
||||
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
||||
import { GenericSkeletonLoader, ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
|
||||
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
|
||||
import * as z from 'zod'
|
||||
|
||||
import {
|
||||
@@ -40,7 +40,6 @@ import {
|
||||
estimateDiskCost,
|
||||
estimateRestoreTime,
|
||||
} from './BranchManagement.utils'
|
||||
import { useIsBranching2Enabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
|
||||
import { BranchingPITRNotice } from '@/components/layouts/AppLayout/EnableBranchingButton/BranchingPITRNotice'
|
||||
import AlertError from '@/components/ui/AlertError'
|
||||
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
|
||||
@@ -70,7 +69,6 @@ export const CreateBranchModal = () => {
|
||||
const { data: selectedOrg } = useSelectedOrganizationQuery()
|
||||
const { showCreateBranchModal, setShowCreateBranchModal } = useAppStateSnapshot()
|
||||
|
||||
const gitlessBranching = useIsBranching2Enabled()
|
||||
const allowDataBranching = useFlag('allowDataBranching')
|
||||
|
||||
const [isGitBranchValid, setIsGitBranchValid] = useState(false)
|
||||
@@ -101,12 +99,7 @@ export const CreateBranchModal = () => {
|
||||
(val) => (branches ?? []).every((branch) => branch.name !== val),
|
||||
'A branch with this name already exists'
|
||||
),
|
||||
gitBranchName: z
|
||||
.string()
|
||||
.refine(
|
||||
(val) => gitlessBranching || !githubConnection || (val && val.length > 0),
|
||||
'Git branch name is required when GitHub is connected'
|
||||
),
|
||||
gitBranchName: z.string().optional(),
|
||||
withData: z.boolean().default(false).optional(),
|
||||
})
|
||||
|
||||
@@ -137,11 +130,10 @@ export const CreateBranchModal = () => {
|
||||
{ enabled: showCreateBranchModal }
|
||||
)
|
||||
const computeAddon = addons?.selected_addons.find((addon) => addon.type === 'compute_instance')
|
||||
const computeSize = !!computeAddon
|
||||
const computeSize = computeAddon
|
||||
? (computeAddon.variant.identifier.split('ci_')[1] as DesiredInstanceSize)
|
||||
: undefined
|
||||
const hasPitrEnabled =
|
||||
(addons?.selected_addons ?? []).find((addon) => addon.type === 'pitr') !== undefined
|
||||
const hasPitrEnabled = (addons?.selected_addons ?? []).some((addon) => addon.type === 'pitr')
|
||||
|
||||
const {
|
||||
data: disk,
|
||||
@@ -174,15 +166,13 @@ export const CreateBranchModal = () => {
|
||||
onSuccess: async (data) => {
|
||||
toast.success(`Successfully created preview branch "${data.name}"`)
|
||||
if (projectRef) {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: projectKeys.detail(projectRef) }),
|
||||
])
|
||||
await queryClient.invalidateQueries({ queryKey: projectKeys.detail(projectRef) })
|
||||
}
|
||||
sendEvent({
|
||||
action: 'branch_create_button_clicked',
|
||||
properties: {
|
||||
branchType: data.persistent ? 'persistent' : 'preview',
|
||||
gitlessBranching,
|
||||
gitlessBranching: !data.git_branch,
|
||||
},
|
||||
groups: {
|
||||
project: ref ?? 'Unknown',
|
||||
@@ -208,18 +198,13 @@ export const CreateBranchModal = () => {
|
||||
!isFormValid ||
|
||||
!canCreateBranch ||
|
||||
!isSuccessAddons ||
|
||||
!isSuccessConnections ||
|
||||
(!!gitBranchName && !isSuccessConnections) ||
|
||||
isLoadingEntitlement ||
|
||||
!hasAccessToBranching ||
|
||||
(!gitlessBranching && !githubConnection) ||
|
||||
isCreatingBranch ||
|
||||
isCheckingGHBranchValidity
|
||||
|
||||
const tooltipText = promptPlanUpgrade
|
||||
? 'Upgrade to unlock branching'
|
||||
: !gitlessBranching && !githubConnection
|
||||
? 'Set up a GitHub connection first to create branches'
|
||||
: undefined
|
||||
const tooltipText = promptPlanUpgrade ? 'Upgrade to unlock branching' : undefined
|
||||
|
||||
const validateGitBranchName = useCallback(
|
||||
(branchName: string) => {
|
||||
@@ -283,24 +268,16 @@ export const CreateBranchModal = () => {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (form && showCreateBranchModal) {
|
||||
form.reset()
|
||||
}
|
||||
if (showCreateBranchModal) form.reset()
|
||||
}, [form, showCreateBranchModal])
|
||||
|
||||
useEffect(() => {
|
||||
if (!githubConnection || !debouncedGitBranchName) {
|
||||
setIsGitBranchValid(gitlessBranching)
|
||||
form.clearErrors('gitBranchName')
|
||||
return
|
||||
}
|
||||
|
||||
form.clearErrors('gitBranchName')
|
||||
validateGitBranchName(debouncedGitBranchName)
|
||||
}, [debouncedGitBranchName, validateGitBranchName, form, githubConnection, gitlessBranching])
|
||||
if (githubConnection && debouncedGitBranchName) validateGitBranchName(debouncedGitBranchName)
|
||||
}, [debouncedGitBranchName, validateGitBranchName, form, githubConnection])
|
||||
|
||||
return (
|
||||
<Dialog open={showCreateBranchModal} onOpenChange={(open) => setShowCreateBranchModal(open)}>
|
||||
<Dialog open={showCreateBranchModal} onOpenChange={setShowCreateBranchModal}>
|
||||
<DialogContent
|
||||
size="large"
|
||||
hideClose
|
||||
@@ -348,92 +325,91 @@ export const CreateBranchModal = () => {
|
||||
)}
|
||||
/>
|
||||
|
||||
{githubConnection && (
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="gitBranchName"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout
|
||||
label={
|
||||
<div className="flex items-center justify-between w-full gap-4">
|
||||
<span className="flex-1">
|
||||
Sync with Git branch {gitlessBranching ? '(optional)' : ''}
|
||||
</span>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Image
|
||||
className={cn('dark:invert')}
|
||||
src={`${BASE_PATH}/img/icons/github-icon.svg`}
|
||||
width={16}
|
||||
height={16}
|
||||
alt={`GitHub icon`}
|
||||
/>
|
||||
<Link
|
||||
href={`https://github.com/${repoOwner}/${repoName}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-foreground hover:underline"
|
||||
>
|
||||
{repoOwner}/{repoName}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
description="Automatically deploy changes on every commit"
|
||||
>
|
||||
<div className="relative w-full">
|
||||
<FormControl_Shadcn_>
|
||||
<Input_Shadcn_
|
||||
{...field}
|
||||
placeholder="e.g. main, feat/some-feature"
|
||||
autoComplete="off"
|
||||
onChange={(e) => {
|
||||
field.onChange(e)
|
||||
setIsGitBranchValid(false)
|
||||
}}
|
||||
/>
|
||||
</FormControl_Shadcn_>
|
||||
<div className="absolute top-2.5 right-3 flex items-center gap-2">
|
||||
{field.value ? (
|
||||
isCheckingGHBranchValidity ? (
|
||||
<Loader2 size={14} className="animate-spin" />
|
||||
) : isGitBranchValid ? (
|
||||
<Check size={14} className="text-brand" strokeWidth={2} />
|
||||
) : null
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
{isLoadingConnections && (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<ShimmeringLoader />
|
||||
<ShimmeringLoader className="w-1/2" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isLoadingConnections && <GenericSkeletonLoader />}
|
||||
{isErrorConnections && (
|
||||
<AlertError
|
||||
error={connectionsError}
|
||||
subject="Failed to retrieve GitHub connection information"
|
||||
/>
|
||||
)}
|
||||
{isSuccessConnections && (
|
||||
<>
|
||||
{!githubConnection && (
|
||||
<div className="flex items-center gap-2 justify-between">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Label>Sync with a GitHub branch</Label>
|
||||
{!gitlessBranching && <Badge variant="warning">Required</Badge>}
|
||||
|
||||
{isSuccessConnections &&
|
||||
(githubConnection ? (
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="gitBranchName"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout
|
||||
label={
|
||||
<div className="flex items-center justify-between w-full gap-4">
|
||||
<span className="flex-1">Sync with Git branch</span>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Image
|
||||
className={cn('dark:invert')}
|
||||
src={`${BASE_PATH}/img/icons/github-icon.svg`}
|
||||
width={16}
|
||||
height={16}
|
||||
alt={`GitHub icon`}
|
||||
/>
|
||||
<Link
|
||||
href={`https://github.com/${repoOwner}/${repoName}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-foreground hover:underline"
|
||||
>
|
||||
{repoOwner}/{repoName}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
labelOptional="Optional"
|
||||
description="Automatically deploy changes on every commit"
|
||||
>
|
||||
<div className="relative w-full">
|
||||
<FormControl_Shadcn_>
|
||||
<Input_Shadcn_
|
||||
{...field}
|
||||
placeholder="e.g. main, feat/some-feature"
|
||||
autoComplete="off"
|
||||
onChange={(e) => {
|
||||
field.onChange(e)
|
||||
setIsGitBranchValid(false)
|
||||
}}
|
||||
/>
|
||||
</FormControl_Shadcn_>
|
||||
<div className="absolute top-2.5 right-3 flex items-center gap-2">
|
||||
{field.value ? (
|
||||
isCheckingGHBranchValidity ? (
|
||||
<Loader2 size={14} className="animate-spin" />
|
||||
) : isGitBranchValid ? (
|
||||
<Check size={14} className="text-brand" strokeWidth={2} />
|
||||
) : null
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-foreground-lighter">
|
||||
Keep this preview branch in sync with a chosen GitHub branch
|
||||
</p>
|
||||
</div>
|
||||
<Button type="default" icon={<Github />} onClick={handleGitHubClick}>
|
||||
Configure
|
||||
</Button>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex items-center gap-2 justify-between">
|
||||
<div className="flex flex-col gap-1">
|
||||
<Label>Sync with a GitHub branch</Label>
|
||||
<p className="text-sm text-foreground-lighter">
|
||||
Keep this preview branch in sync with a chosen GitHub branch
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<Button type="default" icon={<Github />} onClick={handleGitHubClick}>
|
||||
Configure
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{allowDataBranching && (
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
@@ -529,7 +505,7 @@ export const CreateBranchModal = () => {
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<p className="w-24">Targer disk size:</p>
|
||||
<p className="w-24">Target disk size:</p>
|
||||
<p className="w-16">{branchDiskAttributes.size_gb} GB</p>
|
||||
<p>(${estimatedDiskCost.size.toFixed(2)})</p>
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,6 @@ import { useCallback, useEffect, useState } from 'react'
|
||||
import { useForm, useWatch } from 'react-hook-form'
|
||||
import { toast } from 'sonner'
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
cn,
|
||||
Dialog,
|
||||
@@ -25,10 +24,9 @@ import {
|
||||
Label_Shadcn_ as Label,
|
||||
} from 'ui'
|
||||
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
||||
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
|
||||
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
|
||||
import * as z from 'zod'
|
||||
|
||||
import { useIsBranching2Enabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
|
||||
import { AlertError } from '@/components/ui/AlertError'
|
||||
import { InlineLink } from '@/components/ui/InlineLink'
|
||||
import { useBranchUpdateMutation } from '@/data/branches/branch-update-mutation'
|
||||
@@ -50,9 +48,8 @@ export const EditBranchModal = ({ branch, visible, onClose }: EditBranchModalPro
|
||||
const router = useRouter()
|
||||
const { data: projectDetails } = useSelectedProjectQuery()
|
||||
const { data: selectedOrg } = useSelectedOrganizationQuery()
|
||||
const gitlessBranching = useIsBranching2Enabled()
|
||||
|
||||
const [isGitBranchValid, setIsGitBranchValid] = useState(false)
|
||||
const [isGitBranchValid, setIsGitBranchValid] = useState(true)
|
||||
|
||||
const isBranch = projectDetails?.parent_project_ref !== undefined
|
||||
const projectRef =
|
||||
@@ -101,12 +98,7 @@ export const EditBranchModal = ({ branch, visible, onClose }: EditBranchModalPro
|
||||
val === branch?.name || (branches ?? []).every((b) => b.name !== val),
|
||||
'A branch with this name already exists'
|
||||
),
|
||||
gitBranchName: z
|
||||
.string()
|
||||
.refine(
|
||||
(val) => gitlessBranching || !githubConnection || (val && val.length > 0),
|
||||
'Git branch name is required when GitHub is connected'
|
||||
),
|
||||
gitBranchName: z.string().optional(),
|
||||
})
|
||||
|
||||
const form = useForm<z.infer<typeof FormSchema>>({
|
||||
@@ -200,24 +192,21 @@ export const EditBranchModal = ({ branch, visible, onClose }: EditBranchModalPro
|
||||
// Pre-fill form when the modal becomes visible and branch data is available
|
||||
useEffect(() => {
|
||||
if (visible && branch) {
|
||||
setIsGitBranchValid(!!branch.git_branch || gitlessBranching) // Initial validity based on existing link or gitless branching
|
||||
form.reset({
|
||||
branchName: branch.name ?? '',
|
||||
gitBranchName: branch.git_branch ?? '',
|
||||
})
|
||||
}
|
||||
}, [branch, visible, form, gitlessBranching])
|
||||
}, [branch, visible, form])
|
||||
|
||||
useEffect(() => {
|
||||
if (!githubConnection || !debouncedGitBranchName) {
|
||||
setIsGitBranchValid(gitlessBranching)
|
||||
form.clearErrors('gitBranchName')
|
||||
return
|
||||
return form.clearErrors('gitBranchName')
|
||||
}
|
||||
|
||||
form.clearErrors('gitBranchName')
|
||||
validateGitBranchName(debouncedGitBranchName)
|
||||
}, [debouncedGitBranchName, validateGitBranchName, form, githubConnection, gitlessBranching])
|
||||
}, [debouncedGitBranchName, validateGitBranchName, form, githubConnection])
|
||||
|
||||
return (
|
||||
<Dialog open={visible} onOpenChange={(open) => !open && onClose()}>
|
||||
@@ -245,88 +234,88 @@ export const EditBranchModal = ({ branch, visible, onClose }: EditBranchModalPro
|
||||
)}
|
||||
/>
|
||||
|
||||
{githubConnection && (
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="gitBranchName"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout
|
||||
label={
|
||||
<div className="flex items-center justify-between w-full gap-4">
|
||||
<span className="flex-1">
|
||||
Sync with Git branch {gitlessBranching ? '(optional)' : ''}
|
||||
</span>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Image
|
||||
className={cn('dark:invert')}
|
||||
src={`${BASE_PATH}/img/icons/github-icon.svg`}
|
||||
width={16}
|
||||
height={16}
|
||||
alt={`GitHub icon`}
|
||||
/>
|
||||
<InlineLink href={`https://github.com/${repoOwner}/${repoName}`}>
|
||||
{repoOwner}/{repoName}
|
||||
</InlineLink>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
description={
|
||||
gitlessBranching
|
||||
? 'Automatically deploy changes on every commit'
|
||||
: 'If linked, migrations from this Git branch will be automatically deployed.'
|
||||
}
|
||||
>
|
||||
<div className="relative">
|
||||
<FormControl_Shadcn_>
|
||||
<Input_Shadcn_
|
||||
{...field}
|
||||
placeholder="e.g. main, feat/some-feature"
|
||||
autoComplete="off"
|
||||
onChange={(e) => {
|
||||
field.onChange(e)
|
||||
setIsGitBranchValid(false)
|
||||
}}
|
||||
/>
|
||||
</FormControl_Shadcn_>
|
||||
<div className="absolute top-2.5 right-3 flex items-center gap-2">
|
||||
{field.value ? (
|
||||
isChecking ? (
|
||||
<Loader2 size={14} className="animate-spin" />
|
||||
) : isGitBranchValid ? (
|
||||
<Check size={14} className="text-brand" strokeWidth={2} />
|
||||
) : null
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
{isLoadingConnections && (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<ShimmeringLoader />
|
||||
<ShimmeringLoader className="w-1/2" />
|
||||
</div>
|
||||
)}
|
||||
{isLoadingConnections && <GenericSkeletonLoader />}
|
||||
|
||||
{isErrorConnections && (
|
||||
<AlertError
|
||||
error={connectionsError}
|
||||
subject="Failed to retrieve GitHub connection information"
|
||||
/>
|
||||
)}
|
||||
{isSuccessConnections && !githubConnection && (
|
||||
<div className="flex items-center gap-2 justify-between">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Label>Sync with a GitHub branch</Label>
|
||||
{!gitlessBranching && <Badge variant="warning">Required</Badge>}
|
||||
|
||||
{isSuccessConnections &&
|
||||
(githubConnection ? (
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="gitBranchName"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout
|
||||
label={
|
||||
<div className="flex items-center justify-between w-full gap-4">
|
||||
<span className="flex-1">Sync with Git branch</span>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Image
|
||||
className={cn('dark:invert')}
|
||||
src={`${BASE_PATH}/img/icons/github-icon.svg`}
|
||||
width={16}
|
||||
height={16}
|
||||
alt={`GitHub icon`}
|
||||
/>
|
||||
<InlineLink href={`https://github.com/${repoOwner}/${repoName}`}>
|
||||
{repoOwner}/{repoName}
|
||||
</InlineLink>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
labelOptional="Optional"
|
||||
description="Automatically deploy changes on every commit"
|
||||
>
|
||||
<div className="relative">
|
||||
<FormControl_Shadcn_>
|
||||
<Input_Shadcn_
|
||||
{...field}
|
||||
placeholder="e.g. main, feat/some-feature"
|
||||
autoComplete="off"
|
||||
onChange={(e) => {
|
||||
field.onChange(e)
|
||||
setIsGitBranchValid(false)
|
||||
}}
|
||||
/>
|
||||
</FormControl_Shadcn_>
|
||||
<div className="absolute top-2.5 right-3 flex items-center gap-2">
|
||||
{field.value ? (
|
||||
isChecking ? (
|
||||
<Loader2 size={14} className="animate-spin" />
|
||||
) : isGitBranchValid ? (
|
||||
<Check size={14} className="text-brand" strokeWidth={2} />
|
||||
) : null
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex items-center gap-2 justify-between">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Label>Sync with a GitHub branch</Label>
|
||||
</div>
|
||||
<p className="text-sm text-foreground-light">
|
||||
Optionally connect to a GitHub repository to manage migrations automatically
|
||||
for this branch.
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-sm text-foreground-light">
|
||||
{gitlessBranching
|
||||
? 'Optionally connect to a GitHub repository to manage migrations automatically for this branch.'
|
||||
: 'Keep this preview branch in sync with a chosen GitHub branch'}
|
||||
</p>
|
||||
<Button type="default" icon={<Github />} onClick={openLinkerPanel}>
|
||||
Connect to GitHub
|
||||
</Button>
|
||||
</div>
|
||||
<Button type="default" icon={<Github />} onClick={openLinkerPanel}>
|
||||
{gitlessBranching ? 'Connect to GitHub' : 'Configure'}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
))}
|
||||
</DialogSection>
|
||||
|
||||
<DialogFooter padding="medium">
|
||||
@@ -336,11 +325,10 @@ export const EditBranchModal = ({ branch, visible, onClose }: EditBranchModalPro
|
||||
<Button
|
||||
form={formId}
|
||||
disabled={
|
||||
!isSuccessConnections ||
|
||||
(!!gitBranchName && !isSuccessConnections) ||
|
||||
isUpdating ||
|
||||
!canSubmit ||
|
||||
isChecking ||
|
||||
(!gitlessBranching && !githubConnection)
|
||||
isChecking
|
||||
}
|
||||
loading={isUpdating}
|
||||
type="primary"
|
||||
|
||||
@@ -15,16 +15,14 @@ export const PullRequestsEmptyState = ({
|
||||
branches,
|
||||
onBranchSelected,
|
||||
isUpdating,
|
||||
githubConnection,
|
||||
gitlessBranching = false,
|
||||
hasGithubConnection,
|
||||
}: {
|
||||
url: string
|
||||
projectRef: string
|
||||
branches: Branch[]
|
||||
onBranchSelected: (branch: Branch) => void
|
||||
isUpdating: boolean
|
||||
githubConnection?: any
|
||||
gitlessBranching: boolean
|
||||
hasGithubConnection?: boolean
|
||||
}) => {
|
||||
return (
|
||||
<div className={EMPTY_STATE_CONTAINER}>
|
||||
@@ -33,7 +31,7 @@ export const PullRequestsEmptyState = ({
|
||||
Create your first merge request to merge changes back to the main branch
|
||||
</p>
|
||||
<div className="flex items-center space-x-2 mt-4">
|
||||
{githubConnection ? (
|
||||
{hasGithubConnection ? (
|
||||
<Button type="outline" asChild icon={<Github />}>
|
||||
<a href={url} target="_blank" rel="noopener noreferrer">
|
||||
Create pull request
|
||||
@@ -44,14 +42,13 @@ export const PullRequestsEmptyState = ({
|
||||
<Link href={`/project/${projectRef}/settings/integrations`}>Connect to GitHub</Link>
|
||||
</Button>
|
||||
)}
|
||||
{gitlessBranching && (
|
||||
<BranchSelector
|
||||
type="outline"
|
||||
branches={branches}
|
||||
onBranchSelected={onBranchSelected}
|
||||
isUpdating={isUpdating}
|
||||
/>
|
||||
)}
|
||||
<BranchSelector
|
||||
type="outline"
|
||||
align="center"
|
||||
branches={branches}
|
||||
onBranchSelected={onBranchSelected}
|
||||
isUpdating={isUpdating}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -28,7 +28,6 @@ import { ConfirmationModal } from 'ui-patterns/Dialogs/ConfirmationModal'
|
||||
import { BranchLoader, BranchManagementSection, BranchRow, BranchRowLoader } from './BranchPanels'
|
||||
import { EditBranchModal } from './EditBranchModal'
|
||||
import { PreviewBranchesEmptyState } from './EmptyStates'
|
||||
import { useIsBranching2Enabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
|
||||
import { DropdownMenuItemTooltip } from '@/components/ui/DropdownMenuItemTooltip'
|
||||
import { TextConfirmModal } from '@/components/ui/TextConfirmModalWrapper'
|
||||
import { useBranchQuery } from '@/data/branches/branch-query'
|
||||
@@ -236,7 +235,6 @@ const PreviewBranchActions = ({
|
||||
onSelectDeleteBranch: () => void
|
||||
generateCreatePullRequestURL: (branchName?: string) => string
|
||||
}) => {
|
||||
const gitlessBranching = useIsBranching2Enabled()
|
||||
const queryClient = useQueryClient()
|
||||
const { project_ref: branchRef, parent_project_ref: projectRef } = branch
|
||||
|
||||
@@ -321,33 +319,30 @@ const PreviewBranchActions = ({
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-56" side="bottom" align="end">
|
||||
{/* Edit Branch (gitless) */}
|
||||
{gitlessBranching && (
|
||||
<DropdownMenuItemTooltip
|
||||
className="gap-x-2"
|
||||
disabled={!canUpdateBranches || !isBranchActiveHealthy || isUpdatingBranch}
|
||||
onSelect={(e) => {
|
||||
e.stopPropagation()
|
||||
setShowEditBranchModal(true)
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setShowEditBranchModal(true)
|
||||
}}
|
||||
tooltip={{
|
||||
content: {
|
||||
side: 'left',
|
||||
text: !canUpdateBranches
|
||||
? 'You need additional permissions to edit branches'
|
||||
: !isBranchActiveHealthy
|
||||
? 'Branch is still initializing. Please wait for it to become healthy before editing.'
|
||||
: undefined,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Pencil size={14} /> Edit branch
|
||||
</DropdownMenuItemTooltip>
|
||||
)}
|
||||
<DropdownMenuItemTooltip
|
||||
className="gap-x-2"
|
||||
disabled={!canUpdateBranches || !isBranchActiveHealthy || isUpdatingBranch}
|
||||
onSelect={(e) => {
|
||||
e.stopPropagation()
|
||||
setShowEditBranchModal(true)
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setShowEditBranchModal(true)
|
||||
}}
|
||||
tooltip={{
|
||||
content: {
|
||||
side: 'left',
|
||||
text: !canUpdateBranches
|
||||
? 'You need additional permissions to edit branches'
|
||||
: !isBranchActiveHealthy
|
||||
? 'Branch is still initializing. Please wait for it to become healthy before editing.'
|
||||
: undefined,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Pencil size={14} /> Edit branch
|
||||
</DropdownMenuItemTooltip>
|
||||
|
||||
{!branch.deletion_scheduled_at && (
|
||||
<DropdownMenuItemTooltip
|
||||
|
||||
@@ -15,10 +15,7 @@ import { HeaderUpgradeButton } from './HeaderUpgradeButton'
|
||||
import { HomeIcon } from './HomeIcon'
|
||||
import { LocalVersionPopover } from './LocalVersionPopover'
|
||||
import { MergeRequestButton } from './MergeRequestButton'
|
||||
import {
|
||||
useIsBranching2Enabled,
|
||||
useIsFloatingMobileToolbarEnabled,
|
||||
} from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
|
||||
import { useIsFloatingMobileToolbarEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
|
||||
import { ConnectButton } from '@/components/interfaces/ConnectButton/ConnectButton'
|
||||
import { ConnectSheet } from '@/components/interfaces/ConnectSheet/ConnectSheet'
|
||||
import { LocalDropdown } from '@/components/interfaces/LocalDropdown'
|
||||
@@ -73,7 +70,6 @@ export const LayoutHeader = ({
|
||||
const { ref: projectRef, slug } = useParams()
|
||||
const { data: selectedProject } = useSelectedProjectQuery()
|
||||
const { data: selectedOrganization } = useSelectedOrganizationQuery()
|
||||
const gitlessBranching = useIsBranching2Enabled()
|
||||
|
||||
const showFloatingMobileToolbar = useIsFloatingMobileToolbarEnabled()
|
||||
const [commandMenuEnabled] = useLocalStorageQuery(LOCAL_STORAGE_KEYS.HOTKEY_COMMAND_MENU, true)
|
||||
@@ -217,7 +213,7 @@ export const LayoutHeader = ({
|
||||
ease: 'easeOut',
|
||||
}}
|
||||
>
|
||||
{IS_PLATFORM && gitlessBranching && <MergeRequestButton />}
|
||||
{IS_PLATFORM && <MergeRequestButton />}
|
||||
<ConnectButton buttonType={connectButtonType} />
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
} from 'ui'
|
||||
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
|
||||
|
||||
import { useIsBranching2Enabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
|
||||
import {
|
||||
BranchManagementSection,
|
||||
BranchRow,
|
||||
@@ -44,7 +43,6 @@ const MergeRequestsPage: NextPageWithLayout = () => {
|
||||
const { ref } = useParams()
|
||||
const { data: project } = useSelectedProjectQuery()
|
||||
const { data: selectedOrg } = useSelectedOrganizationQuery()
|
||||
const gitlessBranching = useIsBranching2Enabled()
|
||||
|
||||
const isBranch = project?.parent_project_ref !== undefined
|
||||
const projectRef =
|
||||
@@ -68,7 +66,6 @@ const MergeRequestsPage: NextPageWithLayout = () => {
|
||||
error: branchesError,
|
||||
isPending: isLoadingBranches,
|
||||
isError: isErrorBranches,
|
||||
isSuccess: isSuccessBranches,
|
||||
} = useBranchesQuery({ projectRef })
|
||||
const [[mainBranch], previewBranchesUnsorted] = partition(branches, (branch) => branch.is_default)
|
||||
const previewBranches = previewBranchesUnsorted.sort((a, b) =>
|
||||
@@ -190,30 +187,26 @@ const MergeRequestsPage: NextPageWithLayout = () => {
|
||||
|
||||
{!isError && (
|
||||
<div className="space-y-4">
|
||||
{gitlessBranching && (
|
||||
<>
|
||||
{isBranch && !isCurrentBranchReadyForReview && currentBranch && (
|
||||
<div className="rounded border rounded-lg bg-background px-6 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 text-sm text-foreground-light">
|
||||
<GitMerge strokeWidth={1.5} size={16} className="text-brand" />
|
||||
<span className="text-foreground">{currentBranch.name}</span>
|
||||
last viewed
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
size="tiny"
|
||||
loading={currentBranch && isUpdating}
|
||||
onClick={() =>
|
||||
currentBranch && handleMarkBranchForReview(currentBranch)
|
||||
}
|
||||
>
|
||||
Create merge request
|
||||
</Button>
|
||||
</div>
|
||||
{isBranch && !isCurrentBranchReadyForReview && currentBranch && (
|
||||
<div className="rounded border rounded-lg bg-background px-6 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 text-sm text-foreground-light">
|
||||
<GitMerge strokeWidth={1.5} size={16} className="text-brand" />
|
||||
<span className="text-foreground">{currentBranch.name}</span>
|
||||
last viewed
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
<Button
|
||||
type="primary"
|
||||
size="tiny"
|
||||
loading={currentBranch && isUpdating}
|
||||
onClick={() =>
|
||||
currentBranch && handleMarkBranchForReview(currentBranch)
|
||||
}
|
||||
>
|
||||
Create merge request
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<BranchManagementSection
|
||||
header={`${mergeRequestBranches.length} merge requests`}
|
||||
@@ -297,12 +290,11 @@ const MergeRequestsPage: NextPageWithLayout = () => {
|
||||
) : (
|
||||
<PullRequestsEmptyState
|
||||
url={generateCreatePullRequestURL()}
|
||||
gitlessBranching={gitlessBranching}
|
||||
projectRef={projectRef ?? '_'}
|
||||
branches={previewBranches}
|
||||
onBranchSelected={handleMarkBranchForReview}
|
||||
isUpdating={isUpdating}
|
||||
githubConnection={githubConnection}
|
||||
hasGithubConnection={!!githubConnection}
|
||||
/>
|
||||
)}
|
||||
</BranchManagementSection>
|
||||
@@ -322,7 +314,6 @@ const MergeRequestsPageWrapper = ({ children }: PropsWithChildren<{}>) => {
|
||||
const { ref } = useParams()
|
||||
const { data: project } = useSelectedProjectQuery()
|
||||
const { data: selectedOrg } = useSelectedOrganizationQuery()
|
||||
const gitlessBranching = useIsBranching2Enabled()
|
||||
|
||||
const isBranch = project?.parent_project_ref !== undefined
|
||||
const projectRef =
|
||||
@@ -373,36 +364,36 @@ const MergeRequestsPageWrapper = ({ children }: PropsWithChildren<{}>) => {
|
||||
)
|
||||
}
|
||||
|
||||
const primaryActions = gitlessBranching ? (
|
||||
<BranchSelector
|
||||
branches={previewBranches}
|
||||
onBranchSelected={handleMarkBranchForReview}
|
||||
disabled={!projectRef}
|
||||
isUpdating={isUpdating}
|
||||
/>
|
||||
) : null
|
||||
|
||||
const secondaryActions = (
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Button asChild type="text" icon={<MessageCircle className="text-muted" strokeWidth={1} />}>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
href="https://github.com/orgs/supabase/discussions/18937"
|
||||
>
|
||||
Branching feedback
|
||||
</a>
|
||||
</Button>
|
||||
<DocsButton href={`${DOCS_URL}/guides/platform/branching`} />
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<PageLayout
|
||||
title="Merge requests"
|
||||
subtitle="Review and merge changes from one branch into another"
|
||||
primaryActions={primaryActions}
|
||||
secondaryActions={secondaryActions}
|
||||
primaryActions={
|
||||
<BranchSelector
|
||||
branches={previewBranches}
|
||||
onBranchSelected={handleMarkBranchForReview}
|
||||
disabled={!projectRef}
|
||||
isUpdating={isUpdating}
|
||||
/>
|
||||
}
|
||||
secondaryActions={
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Button
|
||||
asChild
|
||||
type="text"
|
||||
icon={<MessageCircle className="text-muted" strokeWidth={1} />}
|
||||
>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
href="https://github.com/orgs/supabase/discussions/18937"
|
||||
>
|
||||
Branching feedback
|
||||
</a>
|
||||
</Button>
|
||||
<DocsButton href={`${DOCS_URL}/guides/platform/branching`} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</PageLayout>
|
||||
|
||||
@@ -19,7 +19,6 @@ export const LOCAL_STORAGE_KEYS = {
|
||||
UI_PREVIEW_INLINE_EDITOR: 'supabase-ui-preview-inline-editor',
|
||||
UI_PREVIEW_UNIFIED_LOGS: 'supabase-ui-preview-unified-logs',
|
||||
UI_ONBOARDING_NEW_PAGE_SHOWN: 'supabase-ui-onboarding-new-page-shown',
|
||||
UI_PREVIEW_BRANCHING_2_0: 'supabase-ui-branching-2-0',
|
||||
UI_PREVIEW_ADVISOR_RULES: 'supabase-ui-advisor-rules',
|
||||
UI_PREVIEW_QUEUE_OPERATIONS: 'supabase-ui-queue-operations',
|
||||
UI_PREVIEW_TABLE_FILTER_BAR: 'supabase-ui-table-filter-bar',
|
||||
@@ -163,7 +162,6 @@ const LOCAL_STORAGE_KEYS_ALLOWLIST = [
|
||||
LOCAL_STORAGE_KEYS.HIDE_PROMO_TOAST,
|
||||
LOCAL_STORAGE_KEYS.BLOG_VIEW,
|
||||
LOCAL_STORAGE_KEYS.AI_ASSISTANT_MCP_OPT_IN,
|
||||
LOCAL_STORAGE_KEYS.UI_PREVIEW_BRANCHING_2_0,
|
||||
LOCAL_STORAGE_KEYS.LINTER_SHOW_FOOTER,
|
||||
LOCAL_STORAGE_KEYS.SIDEBAR_BEHAVIOR,
|
||||
]
|
||||
|
||||
Reference in new issue
Block a user