Initial public release
This commit is contained in:
@@ -0,0 +1,374 @@
|
||||
import React from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { toast } from 'sonner';
|
||||
import { useGitIdentitiesStore, type GitIdentityProfile } from '@/stores/useGitIdentitiesStore';
|
||||
import {
|
||||
RiUser3Line,
|
||||
RiSaveLine,
|
||||
RiDeleteBinLine,
|
||||
RiGitBranchLine,
|
||||
RiBriefcaseLine,
|
||||
RiHomeLine,
|
||||
RiGraduationCapLine,
|
||||
RiCodeLine,
|
||||
RiInformationLine
|
||||
} from '@remixicon/react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
||||
|
||||
const PROFILE_COLORS = [
|
||||
{ key: 'keyword', label: 'Green', cssVar: 'var(--syntax-keyword)' },
|
||||
{ key: 'error', label: 'Red', cssVar: 'var(--status-error)' },
|
||||
{ key: 'string', label: 'Cyan', cssVar: 'var(--syntax-string)' },
|
||||
{ key: 'function', label: 'Orange', cssVar: 'var(--syntax-function)' },
|
||||
{ key: 'type', label: 'Yellow', cssVar: 'var(--syntax-type)' },
|
||||
];
|
||||
|
||||
const PROFILE_ICONS = [
|
||||
{ key: 'branch', Icon: RiGitBranchLine, label: 'Branch' },
|
||||
{ key: 'briefcase', Icon: RiBriefcaseLine, label: 'Work' },
|
||||
{ key: 'house', Icon: RiHomeLine, label: 'Personal' },
|
||||
{ key: 'graduation', Icon: RiGraduationCapLine, label: 'School' },
|
||||
{ key: 'code', Icon: RiCodeLine, label: 'Code' },
|
||||
];
|
||||
|
||||
export const GitIdentitiesPage: React.FC = () => {
|
||||
const {
|
||||
selectedProfileId,
|
||||
getProfileById,
|
||||
createProfile,
|
||||
updateProfile,
|
||||
deleteProfile,
|
||||
} = useGitIdentitiesStore();
|
||||
|
||||
const selectedProfile = React.useMemo(() =>
|
||||
selectedProfileId && selectedProfileId !== 'new' ? getProfileById(selectedProfileId) : null,
|
||||
[selectedProfileId, getProfileById]
|
||||
);
|
||||
const isNewProfile = selectedProfileId === 'new';
|
||||
const isGlobalProfile = selectedProfileId === 'global';
|
||||
|
||||
const [name, setName] = React.useState('');
|
||||
const [userName, setUserName] = React.useState('');
|
||||
const [userEmail, setUserEmail] = React.useState('');
|
||||
const [sshKey, setSshKey] = React.useState('');
|
||||
const [color, setColor] = React.useState('keyword');
|
||||
const [icon, setIcon] = React.useState('branch');
|
||||
const [isSaving, setIsSaving] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isNewProfile) {
|
||||
|
||||
setName('');
|
||||
setUserName('');
|
||||
setUserEmail('');
|
||||
setSshKey('');
|
||||
setColor('keyword');
|
||||
setIcon('branch');
|
||||
} else if (selectedProfile) {
|
||||
|
||||
setName(selectedProfile.name);
|
||||
setUserName(selectedProfile.userName);
|
||||
setUserEmail(selectedProfile.userEmail);
|
||||
setSshKey(selectedProfile.sshKey || '');
|
||||
setColor(selectedProfile.color || 'keyword');
|
||||
setIcon(selectedProfile.icon || 'branch');
|
||||
}
|
||||
}, [selectedProfile, isNewProfile, selectedProfileId]);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!userName.trim() || !userEmail.trim()) {
|
||||
toast.error('User name and email are required');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSaving(true);
|
||||
|
||||
try {
|
||||
const profileData: Omit<GitIdentityProfile, 'id'> & { id?: string } = {
|
||||
name: name.trim() || userName.trim(),
|
||||
userName: userName.trim(),
|
||||
userEmail: userEmail.trim(),
|
||||
sshKey: sshKey.trim() || null,
|
||||
color,
|
||||
icon,
|
||||
};
|
||||
|
||||
let success: boolean;
|
||||
if (isNewProfile) {
|
||||
success = await createProfile(profileData);
|
||||
} else if (selectedProfileId) {
|
||||
success = await updateProfile(selectedProfileId, profileData);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
if (success) {
|
||||
toast.success(isNewProfile ? 'Profile created successfully' : 'Profile updated successfully');
|
||||
} else {
|
||||
toast.error(isNewProfile ? 'Failed to create profile' : 'Failed to update profile');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving profile:', error);
|
||||
toast.error('An error occurred while saving');
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!selectedProfileId || isNewProfile) return;
|
||||
|
||||
if (!confirm('Are you sure you want to delete this profile?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const success = await deleteProfile(selectedProfileId);
|
||||
if (success) {
|
||||
toast.success('Profile deleted successfully');
|
||||
} else {
|
||||
toast.error('Failed to delete profile');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting profile:', error);
|
||||
toast.error('An error occurred while deleting');
|
||||
}
|
||||
};
|
||||
|
||||
const currentColorValue = React.useMemo(() => {
|
||||
const colorConfig = PROFILE_COLORS.find(c => c.key === color);
|
||||
return colorConfig?.cssVar || 'var(--syntax-keyword)';
|
||||
}, [color]);
|
||||
|
||||
if (!selectedProfileId) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="text-center text-muted-foreground">
|
||||
<RiUser3Line className="mx-auto mb-3 h-12 w-12 opacity-50" />
|
||||
<p className="typography-body">Select a profile from the sidebar</p>
|
||||
<p className="typography-meta mt-1 opacity-75">or create a new one</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollableOverlay outerClassName="h-full" className="mx-auto max-w-3xl space-y-6 p-6">
|
||||
{}
|
||||
<div className="space-y-1">
|
||||
<h1 className="typography-ui-header font-semibold text-lg">
|
||||
{isNewProfile ? 'New Git Profile' : isGlobalProfile ? 'Global Identity' : name || 'Edit Profile'}
|
||||
</h1>
|
||||
<p className="typography-body text-muted-foreground mt-1">
|
||||
{isNewProfile
|
||||
? 'Create a new Git identity profile for your repositories'
|
||||
: isGlobalProfile
|
||||
? 'System-wide Git identity from global configuration (read-only)'
|
||||
: 'Configure Git identity settings for this profile'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{}
|
||||
{!isGlobalProfile && (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<h2 className="typography-ui-header font-semibold text-foreground">Profile Information</h2>
|
||||
<p className="typography-meta text-muted-foreground/80">
|
||||
Basic profile settings and display name
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="typography-ui-label font-medium text-foreground">
|
||||
Display Name
|
||||
</label>
|
||||
<Input
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Work Profile, Personal, etc."
|
||||
/>
|
||||
<p className="typography-meta text-muted-foreground">
|
||||
Friendly name to identify this profile (optional, defaults to user name)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<label className="typography-ui-label font-medium text-foreground">
|
||||
Color
|
||||
</label>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{PROFILE_COLORS.map((c) => (
|
||||
<button
|
||||
key={c.key}
|
||||
onClick={() => setColor(c.key)}
|
||||
className={cn(
|
||||
'w-8 h-8 rounded-lg border-2 transition-all',
|
||||
color === c.key
|
||||
? 'border-foreground scale-110'
|
||||
: 'border-transparent hover:border-border'
|
||||
)}
|
||||
style={{ backgroundColor: c.cssVar }}
|
||||
title={c.label}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="typography-ui-label font-medium text-foreground">
|
||||
Icon
|
||||
</label>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{PROFILE_ICONS.map((i) => {
|
||||
const IconComponent = i.Icon;
|
||||
return (
|
||||
<button
|
||||
key={i.key}
|
||||
onClick={() => setIcon(i.key)}
|
||||
className={cn(
|
||||
'w-8 h-8 rounded-lg border-2 transition-all flex items-center justify-center',
|
||||
icon === i.key
|
||||
? 'border-primary bg-accent scale-110'
|
||||
: 'border-border hover:border-primary/50'
|
||||
)}
|
||||
title={i.label}
|
||||
>
|
||||
<IconComponent
|
||||
className="w-4 h-4"
|
||||
|
||||
style={{ color: currentColorValue }}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{}
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<h2 className="typography-h2 font-semibold text-foreground">Git Configuration</h2>
|
||||
<p className="typography-meta text-muted-foreground/80">
|
||||
Git user settings that will be applied to repositories
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="typography-ui-label font-medium text-foreground flex items-center gap-2">
|
||||
User Name {!isGlobalProfile && <span className="text-destructive">*</span>}
|
||||
<Tooltip delayDuration={1000}>
|
||||
<TooltipTrigger asChild>
|
||||
<RiInformationLine className="h-3.5 w-3.5 text-muted-foreground/60 cursor-help" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent sideOffset={8} className="max-w-xs">
|
||||
The name that will appear in Git commit messages.<br/>
|
||||
This is the author name shown in git log and GitHub/GitLab interfaces.
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</label>
|
||||
<Input
|
||||
value={userName}
|
||||
onChange={(e) => setUserName(e.target.value)}
|
||||
placeholder="John Doe"
|
||||
required={!isGlobalProfile}
|
||||
readOnly={isGlobalProfile}
|
||||
disabled={isGlobalProfile}
|
||||
/>
|
||||
<p className="typography-meta text-muted-foreground">
|
||||
Git user.name configuration value
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="typography-ui-label font-medium text-foreground flex items-center gap-2">
|
||||
User Email {!isGlobalProfile && <span className="text-destructive">*</span>}
|
||||
<Tooltip delayDuration={1000}>
|
||||
<TooltipTrigger asChild>
|
||||
<RiInformationLine className="h-3.5 w-3.5 text-muted-foreground/60 cursor-help" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent sideOffset={8} className="max-w-xs">
|
||||
The email address for Git commits.<br/>
|
||||
This should match your email in GitHub/GitLab<br/>
|
||||
to ensure proper attribution of commits.
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</label>
|
||||
<Input
|
||||
type="email"
|
||||
value={userEmail}
|
||||
onChange={(e) => setUserEmail(e.target.value)}
|
||||
placeholder="john@example.com"
|
||||
required={!isGlobalProfile}
|
||||
readOnly={isGlobalProfile}
|
||||
disabled={isGlobalProfile}
|
||||
/>
|
||||
<p className="typography-meta text-muted-foreground">
|
||||
Git user.email configuration value
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="typography-ui-label font-medium text-foreground flex items-center gap-2">
|
||||
SSH Key Path
|
||||
<Tooltip delayDuration={1000}>
|
||||
<TooltipTrigger asChild>
|
||||
<RiInformationLine className="h-3.5 w-3.5 text-muted-foreground/60 cursor-help" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent sideOffset={8} className="max-w-xs">
|
||||
Path to SSH private key used for Git authentication.<br/>
|
||||
This key will be used for HTTPS and SSH Git operations.<br/>
|
||||
Common paths: ~/.ssh/id_rsa, ~/.ssh/id_ed25519
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</label>
|
||||
<Input
|
||||
value={sshKey}
|
||||
onChange={(e) => setSshKey(e.target.value)}
|
||||
placeholder="/Users/username/.ssh/id_rsa"
|
||||
readOnly={isGlobalProfile}
|
||||
disabled={isGlobalProfile}
|
||||
/>
|
||||
<p className="typography-meta text-muted-foreground">
|
||||
Path to SSH private key for authentication (optional)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{}
|
||||
{!isGlobalProfile && (
|
||||
<div className="flex justify-between border-t border-border/40 pt-4">
|
||||
{!isNewProfile && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
onClick={handleDelete}
|
||||
className="gap-2 h-6 px-2 text-xs"
|
||||
>
|
||||
<RiDeleteBinLine className="h-3 w-3" />
|
||||
Delete Profile
|
||||
</Button>
|
||||
)}
|
||||
<div className={cn('flex gap-2', isNewProfile && 'ml-auto')}>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="default"
|
||||
onClick={handleSave}
|
||||
disabled={isSaving}
|
||||
className="gap-2 h-6 px-2 text-xs"
|
||||
>
|
||||
<RiSaveLine className="h-3 w-3" />
|
||||
{isSaving ? 'Saving...' : 'Save Profile'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollableOverlay>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,240 @@
|
||||
import React from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { toast } from 'sonner';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import {
|
||||
RiAddLine,
|
||||
RiGitBranchLine,
|
||||
RiMore2Line,
|
||||
RiDeleteBinLine,
|
||||
RiBriefcaseLine,
|
||||
RiHomeLine,
|
||||
RiGraduationCapLine,
|
||||
RiCodeLine,
|
||||
RiHeartLine,
|
||||
} from '@remixicon/react';
|
||||
import { useGitIdentitiesStore } from '@/stores/useGitIdentitiesStore';
|
||||
import { useUIStore } from '@/stores/useUIStore';
|
||||
import { useDeviceInfo } from '@/lib/device';
|
||||
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { GitIdentityProfile } from '@/stores/useGitIdentitiesStore';
|
||||
|
||||
const ICON_MAP: Record<string, React.ComponentType<{ className?: string; style?: React.CSSProperties }>> = {
|
||||
branch: RiGitBranchLine,
|
||||
briefcase: RiBriefcaseLine,
|
||||
house: RiHomeLine,
|
||||
graduation: RiGraduationCapLine,
|
||||
code: RiCodeLine,
|
||||
heart: RiHeartLine,
|
||||
};
|
||||
|
||||
const COLOR_MAP: Record<string, string> = {
|
||||
keyword: 'var(--syntax-keyword)',
|
||||
error: 'var(--status-error)',
|
||||
string: 'var(--syntax-string)',
|
||||
function: 'var(--syntax-function)',
|
||||
type: 'var(--syntax-type)',
|
||||
};
|
||||
|
||||
export const GitIdentitiesSidebar: React.FC = () => {
|
||||
const {
|
||||
selectedProfileId,
|
||||
profiles,
|
||||
globalIdentity,
|
||||
setSelectedProfile,
|
||||
deleteProfile,
|
||||
loadProfiles,
|
||||
loadGlobalIdentity,
|
||||
} = useGitIdentitiesStore();
|
||||
|
||||
const { setSidebarOpen } = useUIStore();
|
||||
const { isMobile } = useDeviceInfo();
|
||||
|
||||
React.useEffect(() => {
|
||||
loadProfiles();
|
||||
loadGlobalIdentity();
|
||||
}, [loadProfiles, loadGlobalIdentity]);
|
||||
|
||||
const handleCreateProfile = () => {
|
||||
setSelectedProfile('new');
|
||||
if (isMobile) {
|
||||
setSidebarOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteProfile = async (profile: GitIdentityProfile) => {
|
||||
if (window.confirm(`Are you sure you want to delete profile "${profile.name}"?`)) {
|
||||
const success = await deleteProfile(profile.id);
|
||||
if (success) {
|
||||
toast.success(`Profile "${profile.name}" deleted successfully`);
|
||||
} else {
|
||||
toast.error('Failed to delete profile');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col bg-sidebar">
|
||||
<div className={cn('border-b border-border/40 px-3 dark:border-white/10', isMobile ? 'mt-2 py-3' : 'py-3')}>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<h2 className="typography-ui-label font-semibold text-foreground">Git Profiles</h2>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="typography-meta text-muted-foreground">{profiles.length}</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 text-muted-foreground"
|
||||
onClick={handleCreateProfile}
|
||||
>
|
||||
<RiAddLine className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ScrollableOverlay outerClassName="flex-1 min-h-0" className="space-y-1 px-3 py-2">
|
||||
{}
|
||||
{globalIdentity && (
|
||||
<>
|
||||
<div className="px-2 pb-1.5 pt-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
||||
System Default
|
||||
</div>
|
||||
<ProfileListItem
|
||||
profile={globalIdentity}
|
||||
isSelected={selectedProfileId === 'global'}
|
||||
onSelect={() => {
|
||||
setSelectedProfile('global');
|
||||
if (isMobile) {
|
||||
setSidebarOpen(false);
|
||||
}
|
||||
}}
|
||||
onDelete={undefined}
|
||||
isReadOnly
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{}
|
||||
{profiles.length > 0 && (
|
||||
<div className="px-2 pb-1.5 pt-3 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
||||
Custom Profiles
|
||||
</div>
|
||||
)}
|
||||
|
||||
{profiles.length === 0 && !globalIdentity ? (
|
||||
<div className="py-12 px-4 text-center text-muted-foreground">
|
||||
<RiGitBranchLine className="mx-auto mb-3 h-10 w-10 opacity-50" />
|
||||
<p className="typography-ui-label font-medium">No profiles configured</p>
|
||||
<p className="typography-meta mt-1 opacity-75">Use the + button above to create one</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{profiles.map((profile) => (
|
||||
<ProfileListItem
|
||||
key={profile.id}
|
||||
profile={profile}
|
||||
isSelected={selectedProfileId === profile.id}
|
||||
onSelect={() => {
|
||||
setSelectedProfile(profile.id);
|
||||
if (isMobile) {
|
||||
setSidebarOpen(false);
|
||||
}
|
||||
}}
|
||||
onDelete={() => handleDeleteProfile(profile)}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</ScrollableOverlay>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface ProfileListItemProps {
|
||||
profile: GitIdentityProfile;
|
||||
isSelected: boolean;
|
||||
onSelect: () => void;
|
||||
onDelete?: () => void;
|
||||
isReadOnly?: boolean;
|
||||
}
|
||||
|
||||
const ProfileListItem: React.FC<ProfileListItemProps> = ({
|
||||
profile,
|
||||
isSelected,
|
||||
onSelect,
|
||||
onDelete,
|
||||
isReadOnly = false,
|
||||
}) => {
|
||||
|
||||
const IconComponent = ICON_MAP[profile.icon || 'branch'] || RiGitBranchLine;
|
||||
|
||||
const iconColor = COLOR_MAP[profile.color || ''];
|
||||
|
||||
return (
|
||||
<div className="group transition-all duration-200">
|
||||
<div className="relative">
|
||||
<div className="w-full flex items-center justify-between py-1.5 px-2 pr-1">
|
||||
<button
|
||||
onClick={onSelect}
|
||||
className="flex-1 text-left overflow-hidden"
|
||||
inputMode="none"
|
||||
tabIndex={0}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<IconComponent
|
||||
className="w-4 h-4 flex-shrink-0"
|
||||
style={{ color: iconColor }}
|
||||
/>
|
||||
<div className={cn(
|
||||
"typography-ui-label font-medium truncate flex-1",
|
||||
isSelected
|
||||
? "text-primary"
|
||||
: "text-foreground hover:text-primary/80"
|
||||
)}>
|
||||
{profile.name}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{}
|
||||
<div className="typography-meta text-muted-foreground truncate mt-0.5">
|
||||
{profile.userEmail}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{!isReadOnly && onDelete && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-6 w-6 flex-shrink-0 -mr-1 opacity-100 transition-opacity md:opacity-0 md:group-hover:opacity-100"
|
||||
>
|
||||
<RiMore2Line className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-fit min-w-20">
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDelete();
|
||||
}}
|
||||
className="text-destructive focus:text-destructive"
|
||||
>
|
||||
<RiDeleteBinLine className="h-4 w-4 mr-px" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user