Revert "Unify workspace dropdowns; Update layouts (#458)"
This reverts commit 4bc8448a1d.
This commit is contained in:
@@ -24,7 +24,6 @@
|
||||
"@kit/billing-gateway": "workspace:*",
|
||||
"@kit/email-templates": "workspace:*",
|
||||
"@kit/eslint-config": "workspace:*",
|
||||
"@kit/i18n": "workspace:*",
|
||||
"@kit/mailers": "workspace:*",
|
||||
"@kit/monitoring": "workspace:*",
|
||||
"@kit/next": "workspace:*",
|
||||
@@ -34,18 +33,18 @@
|
||||
"@kit/supabase": "workspace:*",
|
||||
"@kit/tsconfig": "workspace:*",
|
||||
"@kit/ui": "workspace:*",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@supabase/supabase-js": "catalog:",
|
||||
"@tanstack/react-query": "catalog:",
|
||||
"@types/react": "catalog:",
|
||||
"@types/react-dom": "catalog:",
|
||||
"lucide-react": "catalog:",
|
||||
"next": "catalog:",
|
||||
"next-intl": "catalog:",
|
||||
"next-safe-action": "catalog:",
|
||||
"next-themes": "0.4.6",
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
"react-hook-form": "catalog:",
|
||||
"react-i18next": "catalog:",
|
||||
"zod": "catalog:"
|
||||
},
|
||||
"prettier": "@kit/prettier-config",
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import { ChevronsUpDown, Plus, User } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { CaretSortIcon, PersonIcon } from '@radix-ui/react-icons';
|
||||
import { CheckCircle, Plus } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@kit/ui/avatar';
|
||||
import { Button } from '@kit/ui/button';
|
||||
@@ -39,7 +40,7 @@ interface AccountSelectorProps {
|
||||
selectedAccount?: string;
|
||||
collapsed?: boolean;
|
||||
className?: string;
|
||||
showPersonalAccount?: boolean;
|
||||
collisionPadding?: number;
|
||||
|
||||
onAccountChange: (value: string | undefined) => void;
|
||||
}
|
||||
@@ -56,14 +57,16 @@ export function AccountSelector({
|
||||
enableTeamCreation: true,
|
||||
},
|
||||
collapsed = false,
|
||||
showPersonalAccount = true,
|
||||
collisionPadding = 20,
|
||||
}: React.PropsWithChildren<AccountSelectorProps>) {
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
const [isCreatingAccount, setIsCreatingAccount] = useState<boolean>(false);
|
||||
const t = useTranslations('teams');
|
||||
const { t } = useTranslation('teams');
|
||||
const personalData = usePersonalAccountData(userId);
|
||||
|
||||
const value = selectedAccount ?? PERSONAL_ACCOUNT_SLUG;
|
||||
const value = useMemo(() => {
|
||||
return selectedAccount ?? PERSONAL_ACCOUNT_SLUG;
|
||||
}, [selectedAccount]);
|
||||
|
||||
const selected = accounts.find((account) => account.value === value);
|
||||
const pictureUrl = personalData.data?.picture_url;
|
||||
@@ -71,136 +74,128 @@ export function AccountSelector({
|
||||
return (
|
||||
<>
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger
|
||||
render={
|
||||
<Button
|
||||
data-test={'account-selector-trigger'}
|
||||
size={collapsed ? 'icon' : 'default'}
|
||||
variant="ghost"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className={cn(
|
||||
'dark:shadow-primary/10 group mr-1 w-full min-w-0 px-2 lg:w-auto lg:max-w-[185px]',
|
||||
{
|
||||
'justify-start': !collapsed,
|
||||
'm-auto justify-center px-2 lg:w-full': collapsed,
|
||||
},
|
||||
className,
|
||||
)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<If
|
||||
condition={selected}
|
||||
fallback={
|
||||
<span
|
||||
className={cn('flex max-w-full items-center', {
|
||||
'justify-center gap-x-0': collapsed,
|
||||
'gap-x-2': !collapsed,
|
||||
})}
|
||||
>
|
||||
<PersonalAccountAvatar pictureUrl={pictureUrl} />
|
||||
|
||||
<span
|
||||
className={cn('truncate', {
|
||||
hidden: collapsed,
|
||||
})}
|
||||
>
|
||||
<Trans i18nKey={'teams.personalAccount'} />
|
||||
</span>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
{(account) => (
|
||||
<span
|
||||
className={cn('flex max-w-full items-center', {
|
||||
'justify-center gap-x-0': collapsed,
|
||||
'gap-x-2': !collapsed,
|
||||
})}
|
||||
>
|
||||
<Avatar className={'h-6 w-6 rounded-xs'}>
|
||||
<AvatarImage src={account.image ?? undefined} />
|
||||
|
||||
<AvatarFallback
|
||||
className={'group-hover:bg-background rounded-xs'}
|
||||
>
|
||||
{account.label ? account.label[0] : ''}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
|
||||
<span
|
||||
className={cn('truncate', {
|
||||
hidden: collapsed,
|
||||
})}
|
||||
>
|
||||
{account.label}
|
||||
</span>
|
||||
</span>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
data-test={'account-selector-trigger'}
|
||||
size={collapsed ? 'icon' : 'default'}
|
||||
variant="ghost"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className={cn(
|
||||
'dark:shadow-primary/10 group mr-1 w-full min-w-0 px-2 lg:w-auto lg:max-w-fit',
|
||||
{
|
||||
'justify-start': !collapsed,
|
||||
'm-auto justify-center px-2 lg:w-full': collapsed,
|
||||
},
|
||||
className,
|
||||
)}
|
||||
</If>
|
||||
>
|
||||
<If
|
||||
condition={selected}
|
||||
fallback={
|
||||
<span
|
||||
className={cn('flex max-w-full items-center', {
|
||||
'justify-center gap-x-0': collapsed,
|
||||
'gap-x-2': !collapsed,
|
||||
})}
|
||||
>
|
||||
<PersonalAccountAvatar pictureUrl={pictureUrl} />
|
||||
|
||||
<ChevronsUpDown
|
||||
className={cn('ml-1 h-4 w-4 shrink-0 opacity-50', {
|
||||
hidden: collapsed,
|
||||
})}
|
||||
/>
|
||||
<span
|
||||
className={cn('truncate', {
|
||||
hidden: collapsed,
|
||||
})}
|
||||
>
|
||||
<Trans i18nKey={'teams:personalAccount'} />
|
||||
</span>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
{(account) => (
|
||||
<span
|
||||
className={cn('flex max-w-full items-center', {
|
||||
'justify-center gap-x-0': collapsed,
|
||||
'gap-x-2': !collapsed,
|
||||
})}
|
||||
>
|
||||
<Avatar className={'h-6 w-6 rounded-xs'}>
|
||||
<AvatarImage src={account.image ?? undefined} />
|
||||
|
||||
<AvatarFallback
|
||||
className={'group-hover:bg-background rounded-xs'}
|
||||
>
|
||||
{account.label ? account.label[0] : ''}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
|
||||
<span
|
||||
className={cn('truncate', {
|
||||
hidden: collapsed,
|
||||
})}
|
||||
>
|
||||
{account.label}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
</If>
|
||||
|
||||
<CaretSortIcon
|
||||
className={cn('ml-1 h-4 w-4 shrink-0 opacity-50', {
|
||||
hidden: collapsed,
|
||||
})}
|
||||
/>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverContent
|
||||
data-test={'account-selector-content'}
|
||||
className="w-full gap-0 p-0"
|
||||
className="w-full p-0"
|
||||
collisionPadding={collisionPadding}
|
||||
>
|
||||
<Command value={value}>
|
||||
<Command>
|
||||
<CommandInput placeholder={t('searchAccount')} className="h-9" />
|
||||
|
||||
<CommandList>
|
||||
{showPersonalAccount && (
|
||||
<>
|
||||
<CommandGroup>
|
||||
<CommandItem
|
||||
tabIndex={0}
|
||||
value={PERSONAL_ACCOUNT_SLUG}
|
||||
onSelect={() => onAccountChange(undefined)}
|
||||
className={cn('', {
|
||||
'bg-muted': value === PERSONAL_ACCOUNT_SLUG,
|
||||
'hover:bg-muted/50 data-selected:bg-transparent':
|
||||
value !== PERSONAL_ACCOUNT_SLUG,
|
||||
})}
|
||||
>
|
||||
<PersonalAccountAvatar />
|
||||
<CommandGroup>
|
||||
<CommandItem
|
||||
className="shadow-none"
|
||||
onSelect={() => onAccountChange(undefined)}
|
||||
value={PERSONAL_ACCOUNT_SLUG}
|
||||
>
|
||||
<PersonalAccountAvatar />
|
||||
|
||||
<span className={'ml-2'}>
|
||||
<Trans i18nKey={'teams.personalAccount'} />
|
||||
</span>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
<span className={'ml-2'}>
|
||||
<Trans i18nKey={'teams:personalAccount'} />
|
||||
</span>
|
||||
|
||||
<CommandSeparator />
|
||||
</>
|
||||
)}
|
||||
<Icon selected={value === PERSONAL_ACCOUNT_SLUG} />
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
|
||||
<CommandSeparator />
|
||||
|
||||
<If condition={accounts.length > 0}>
|
||||
<CommandGroup
|
||||
heading={
|
||||
<Trans
|
||||
i18nKey={'teams.yourTeams'}
|
||||
i18nKey={'teams:yourTeams'}
|
||||
values={{ teamsCount: accounts.length }}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{(accounts ?? []).map((account) => (
|
||||
<CommandItem
|
||||
className={cn('', {
|
||||
'bg-muted': value === account.value,
|
||||
'hover:bg-muted/50 data-selected:bg-transparent':
|
||||
value !== account.value,
|
||||
})}
|
||||
tabIndex={0}
|
||||
data-test={'account-selector-team'}
|
||||
data-name={account.label}
|
||||
data-slug={account.value}
|
||||
className={cn(
|
||||
'group my-1 flex justify-between shadow-none transition-colors',
|
||||
{
|
||||
['bg-muted']: value === account.value,
|
||||
},
|
||||
)}
|
||||
key={account.value}
|
||||
value={account.value ?? undefined}
|
||||
value={account.value ?? ''}
|
||||
onSelect={(currentValue) => {
|
||||
setOpen(false);
|
||||
|
||||
@@ -209,12 +204,13 @@ export function AccountSelector({
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className={'flex w-full items-center'}>
|
||||
<div className={'flex items-center'}>
|
||||
<Avatar className={'mr-2 h-6 w-6 rounded-xs'}>
|
||||
<AvatarImage src={account.image ?? undefined} />
|
||||
|
||||
<AvatarFallback
|
||||
className={cn('rounded-xs', {
|
||||
['bg-background']: value === account.value,
|
||||
['group-hover:bg-background']:
|
||||
value !== account.value,
|
||||
})}
|
||||
@@ -223,10 +219,12 @@ export function AccountSelector({
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
|
||||
<span className={'max-w-[165px] truncate'}>
|
||||
<span className={'mr-2 max-w-[165px] truncate'}>
|
||||
{account.label}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Icon selected={(account.value ?? '') === value} />
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
@@ -234,27 +232,26 @@ export function AccountSelector({
|
||||
</CommandList>
|
||||
</Command>
|
||||
|
||||
<Separator />
|
||||
|
||||
<If condition={features.enableTeamCreation}>
|
||||
<div className="px-1">
|
||||
<Separator />
|
||||
<div className={'p-1'}>
|
||||
<Button
|
||||
data-test={'create-team-account-trigger'}
|
||||
variant="ghost"
|
||||
size={'sm'}
|
||||
className="w-full justify-start text-sm font-normal"
|
||||
onClick={() => {
|
||||
setIsCreatingAccount(true);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<Plus className="mr-3 h-4 w-4" />
|
||||
|
||||
<div className="py-1">
|
||||
<Button
|
||||
data-test={'create-team-account-trigger'}
|
||||
variant="ghost"
|
||||
className="w-full justify-start text-sm font-normal"
|
||||
onClick={() => {
|
||||
setIsCreatingAccount(true);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<Plus className="mr-3 h-4 w-4" />
|
||||
|
||||
<span>
|
||||
<Trans i18nKey={'teams.createTeam'} />
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
<span>
|
||||
<Trans i18nKey={'teams:createTeam'} />
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
</If>
|
||||
</PopoverContent>
|
||||
@@ -278,10 +275,18 @@ function UserAvatar(props: { pictureUrl?: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
function Icon({ selected }: { selected: boolean }) {
|
||||
return (
|
||||
<CheckCircle
|
||||
className={cn('ml-auto h-4 w-4', selected ? 'opacity-100' : 'opacity-0')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function PersonalAccountAvatar({ pictureUrl }: { pictureUrl?: string | null }) {
|
||||
return pictureUrl ? (
|
||||
<UserAvatar pictureUrl={pictureUrl} />
|
||||
) : (
|
||||
<User className="h-5 w-5" />
|
||||
<PersonIcon className="h-5 w-5" />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
LogOut,
|
||||
MessageCircleQuestion,
|
||||
Shield,
|
||||
User,
|
||||
} from 'lucide-react';
|
||||
|
||||
import { JWTUserData } from '@kit/supabase/types';
|
||||
@@ -50,7 +49,6 @@ export function PersonalAccountDropdown({
|
||||
|
||||
paths: {
|
||||
home: string;
|
||||
profileSettings: string;
|
||||
};
|
||||
|
||||
features: {
|
||||
@@ -89,10 +87,11 @@ export function PersonalAccountDropdown({
|
||||
aria-label="Open your profile menu"
|
||||
data-test={'account-dropdown-trigger'}
|
||||
className={cn(
|
||||
'group/trigger fade-in focus:outline-primary flex cursor-pointer items-center group-data-[collapsible=icon]:px-0',
|
||||
'group/trigger fade-in focus:outline-primary flex cursor-pointer items-center group-data-[minimized=true]/sidebar:px-0',
|
||||
className ?? '',
|
||||
{
|
||||
['active:bg-secondary/50 group-data-[collapsible=none]:hover:bg-secondary items-center gap-4 rounded-md border-dashed p-2 transition-colors group-data-[collapsible=none]:border']:
|
||||
['active:bg-secondary/50 items-center gap-4 rounded-md' +
|
||||
' hover:bg-secondary border border-dashed p-2 transition-colors']:
|
||||
showProfileName,
|
||||
},
|
||||
)}
|
||||
@@ -109,7 +108,7 @@ export function PersonalAccountDropdown({
|
||||
<If condition={showProfileName}>
|
||||
<div
|
||||
className={
|
||||
'fade-in flex w-full flex-col truncate text-left group-data-[collapsible=icon]:hidden'
|
||||
'fade-in flex w-full flex-col truncate text-left group-data-[minimized=true]/sidebar:hidden'
|
||||
}
|
||||
>
|
||||
<span
|
||||
@@ -129,7 +128,7 @@ export function PersonalAccountDropdown({
|
||||
|
||||
<ChevronsUpDown
|
||||
className={
|
||||
'text-muted-foreground mr-1 h-8 group-data-[collapsible=icon]:hidden'
|
||||
'text-muted-foreground mr-1 h-8 group-data-[minimized=true]/sidebar:hidden'
|
||||
}
|
||||
/>
|
||||
</If>
|
||||
@@ -141,7 +140,7 @@ export function PersonalAccountDropdown({
|
||||
className={'flex flex-col justify-start truncate text-left text-xs'}
|
||||
>
|
||||
<div className={'text-muted-foreground'}>
|
||||
<Trans i18nKey={'common.signedInAs'} />
|
||||
<Trans i18nKey={'common:signedInAs'} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -152,69 +151,48 @@ export function PersonalAccountDropdown({
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuItem
|
||||
render={
|
||||
<Link
|
||||
className={'s-full flex cursor-pointer items-center space-x-2'}
|
||||
href={paths.home}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Home className={'h-5'} />
|
||||
<DropdownMenuItem asChild>
|
||||
<Link
|
||||
className={'s-full flex cursor-pointer items-center space-x-2'}
|
||||
href={paths.home}
|
||||
>
|
||||
<Home className={'h-5'} />
|
||||
|
||||
<span>
|
||||
<Trans i18nKey={'common.routes.home'} />
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem
|
||||
render={
|
||||
<Link
|
||||
className={'s-full flex cursor-pointer items-center space-x-2'}
|
||||
href={paths.profileSettings}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<User className={'h-5'} />
|
||||
|
||||
<span>
|
||||
<Trans i18nKey={'common.routes.profile'} />
|
||||
</span>
|
||||
<span>
|
||||
<Trans i18nKey={'common:routes.home'} />
|
||||
</span>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuItem
|
||||
render={
|
||||
<Link
|
||||
className={'s-full flex cursor-pointer items-center space-x-2'}
|
||||
href={'/docs'}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<MessageCircleQuestion className={'h-5'} />
|
||||
<DropdownMenuItem asChild>
|
||||
<Link
|
||||
className={'s-full flex cursor-pointer items-center space-x-2'}
|
||||
href={'/docs'}
|
||||
>
|
||||
<MessageCircleQuestion className={'h-5'} />
|
||||
|
||||
<span>
|
||||
<Trans i18nKey={'common.documentation'} />
|
||||
</span>
|
||||
<span>
|
||||
<Trans i18nKey={'common:documentation'} />
|
||||
</span>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<If condition={isSuperAdmin}>
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuItem
|
||||
render={
|
||||
<Link
|
||||
className={
|
||||
's-full flex cursor-pointer items-center space-x-2 text-yellow-700 dark:text-yellow-500'
|
||||
}
|
||||
href={'/admin'}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Shield className={'h-5'} />
|
||||
<DropdownMenuItem asChild>
|
||||
<Link
|
||||
className={
|
||||
's-full flex cursor-pointer items-center space-x-2 text-yellow-700 dark:text-yellow-500'
|
||||
}
|
||||
href={'/admin'}
|
||||
>
|
||||
<Shield className={'h-5'} />
|
||||
|
||||
<span>Super Admin</span>
|
||||
<span>Super Admin</span>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
</If>
|
||||
|
||||
@@ -236,7 +214,7 @@ export function PersonalAccountDropdown({
|
||||
<LogOut className={'h-5'} />
|
||||
|
||||
<span>
|
||||
<Trans i18nKey={'auth.signOut'} />
|
||||
<Trans i18nKey={'auth:signOut'} />
|
||||
</span>
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { useFormStatus } from 'react-dom';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { TriangleAlert } from 'lucide-react';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { ExclamationTriangleIcon } from '@radix-ui/react-icons';
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
|
||||
import { ErrorBoundary } from '@kit/monitoring/components';
|
||||
@@ -30,11 +31,11 @@ export function AccountDangerZone() {
|
||||
<div className={'flex flex-col space-y-4'}>
|
||||
<div className={'flex flex-col space-y-1'}>
|
||||
<span className={'text-sm font-medium'}>
|
||||
<Trans i18nKey={'account.deleteAccount'} />
|
||||
<Trans i18nKey={'account:deleteAccount'} />
|
||||
</span>
|
||||
|
||||
<p className={'text-muted-foreground text-sm'}>
|
||||
<Trans i18nKey={'account.deleteAccountDescription'} />
|
||||
<Trans i18nKey={'account:deleteAccountDescription'} />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -54,18 +55,16 @@ function DeleteAccountModal() {
|
||||
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger
|
||||
render={
|
||||
<Button data-test={'delete-account-button'} variant={'destructive'}>
|
||||
<Trans i18nKey={'account.deleteAccount'} />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button data-test={'delete-account-button'} variant={'destructive'}>
|
||||
<Trans i18nKey={'account:deleteAccount'} />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
|
||||
<AlertDialogContent>
|
||||
<AlertDialogContent onEscapeKeyDown={(e) => e.preventDefault()}>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
<Trans i18nKey={'account.deleteAccount'} />
|
||||
<Trans i18nKey={'account:deleteAccount'} />
|
||||
</AlertDialogTitle>
|
||||
</AlertDialogHeader>
|
||||
|
||||
@@ -78,8 +77,6 @@ function DeleteAccountModal() {
|
||||
}
|
||||
|
||||
function DeleteAccountForm(props: { email: string }) {
|
||||
const { execute, isPending } = useAction(deletePersonalAccountAction);
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(DeletePersonalAccountSchema),
|
||||
defaultValues: {
|
||||
@@ -97,7 +94,7 @@ function DeleteAccountForm(props: { email: string }) {
|
||||
onSuccess={(otp) => form.setValue('otp', otp, { shouldValidate: true })}
|
||||
CancelButton={
|
||||
<AlertDialogCancel>
|
||||
<Trans i18nKey={'common.cancel'} />
|
||||
<Trans i18nKey={'common:cancel'} />
|
||||
</AlertDialogCancel>
|
||||
}
|
||||
/>
|
||||
@@ -108,12 +105,11 @@ function DeleteAccountForm(props: { email: string }) {
|
||||
<Form {...form}>
|
||||
<form
|
||||
data-test={'delete-account-form'}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
execute({ otp });
|
||||
}}
|
||||
action={deletePersonalAccountAction}
|
||||
className={'flex flex-col space-y-4'}
|
||||
>
|
||||
<input type="hidden" name="otp" value={otp} />
|
||||
|
||||
<div className={'flex flex-col space-y-6'}>
|
||||
<div
|
||||
className={
|
||||
@@ -122,11 +118,11 @@ function DeleteAccountForm(props: { email: string }) {
|
||||
>
|
||||
<div className={'flex flex-col space-y-2'}>
|
||||
<div>
|
||||
<Trans i18nKey={'account.deleteAccountDescription'} />
|
||||
<Trans i18nKey={'account:deleteAccountDescription'} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Trans i18nKey={'common.modalConfirmationQuestion'} />
|
||||
<Trans i18nKey={'common:modalConfirmationQuestion'} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -134,28 +130,36 @@ function DeleteAccountForm(props: { email: string }) {
|
||||
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>
|
||||
<Trans i18nKey={'common.cancel'} />
|
||||
<Trans i18nKey={'common:cancel'} />
|
||||
</AlertDialogCancel>
|
||||
|
||||
<Button
|
||||
data-test={'confirm-delete-account-button'}
|
||||
type={'submit'}
|
||||
disabled={isPending || !form.formState.isValid}
|
||||
name={'action'}
|
||||
variant={'destructive'}
|
||||
>
|
||||
{isPending ? (
|
||||
<Trans i18nKey={'account.deletingAccount'} />
|
||||
) : (
|
||||
<Trans i18nKey={'account.deleteAccount'} />
|
||||
)}
|
||||
</Button>
|
||||
<DeleteAccountSubmitButton disabled={!form.formState.isValid} />
|
||||
</AlertDialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
function DeleteAccountSubmitButton(props: { disabled: boolean }) {
|
||||
const { pending } = useFormStatus();
|
||||
|
||||
return (
|
||||
<Button
|
||||
data-test={'confirm-delete-account-button'}
|
||||
type={'submit'}
|
||||
disabled={pending || props.disabled}
|
||||
name={'action'}
|
||||
variant={'destructive'}
|
||||
>
|
||||
{pending ? (
|
||||
<Trans i18nKey={'account:deletingAccount'} />
|
||||
) : (
|
||||
<Trans i18nKey={'account:deleteAccount'} />
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function DeleteAccountErrorContainer() {
|
||||
return (
|
||||
<div className="flex flex-col gap-y-4">
|
||||
@@ -163,7 +167,7 @@ function DeleteAccountErrorContainer() {
|
||||
|
||||
<div>
|
||||
<AlertDialogCancel>
|
||||
<Trans i18nKey={'common.cancel'} />
|
||||
<Trans i18nKey={'common:cancel'} />
|
||||
</AlertDialogCancel>
|
||||
</div>
|
||||
</div>
|
||||
@@ -173,14 +177,14 @@ function DeleteAccountErrorContainer() {
|
||||
function DeleteAccountErrorAlert() {
|
||||
return (
|
||||
<Alert variant={'destructive'}>
|
||||
<TriangleAlert className={'h-4'} />
|
||||
<ExclamationTriangleIcon className={'h-4'} />
|
||||
|
||||
<AlertTitle>
|
||||
<Trans i18nKey={'account.deleteAccountErrorHeading'} />
|
||||
<Trans i18nKey={'account:deleteAccountErrorHeading'} />
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
<Trans i18nKey={'common.genericError'} />
|
||||
<Trans i18nKey={'common:genericError'} />
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
import type { Provider } from '@supabase/supabase-js';
|
||||
|
||||
import { routing } from '@kit/i18n';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -54,11 +55,11 @@ export function PersonalAccountSettingsContainer(
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
<Trans i18nKey={'account.accountImage'} />
|
||||
<Trans i18nKey={'account:accountImage'} />
|
||||
</CardTitle>
|
||||
|
||||
<CardDescription>
|
||||
<Trans i18nKey={'account.accountImageDescription'} />
|
||||
<Trans i18nKey={'account:accountImageDescription'} />
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
@@ -75,11 +76,11 @@ export function PersonalAccountSettingsContainer(
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
<Trans i18nKey={'account.name'} />
|
||||
<Trans i18nKey={'account:name'} />
|
||||
</CardTitle>
|
||||
|
||||
<CardDescription>
|
||||
<Trans i18nKey={'account.nameDescription'} />
|
||||
<Trans i18nKey={'account:nameDescription'} />
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
@@ -92,16 +93,16 @@ export function PersonalAccountSettingsContainer(
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
<Trans i18nKey={'account.language'} />
|
||||
<Trans i18nKey={'account:language'} />
|
||||
</CardTitle>
|
||||
|
||||
<CardDescription>
|
||||
<Trans i18nKey={'account.languageDescription'} />
|
||||
<Trans i18nKey={'account:languageDescription'} />
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<LanguageSelector locales={routing.locales} />
|
||||
<LanguageSelector />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</If>
|
||||
@@ -109,11 +110,11 @@ export function PersonalAccountSettingsContainer(
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
<Trans i18nKey={'account.updateEmailCardTitle'} />
|
||||
<Trans i18nKey={'account:updateEmailCardTitle'} />
|
||||
</CardTitle>
|
||||
|
||||
<CardDescription>
|
||||
<Trans i18nKey={'account.updateEmailCardDescription'} />
|
||||
<Trans i18nKey={'account:updateEmailCardDescription'} />
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
@@ -126,11 +127,11 @@ export function PersonalAccountSettingsContainer(
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
<Trans i18nKey={'account.updatePasswordCardTitle'} />
|
||||
<Trans i18nKey={'account:updatePasswordCardTitle'} />
|
||||
</CardTitle>
|
||||
|
||||
<CardDescription>
|
||||
<Trans i18nKey={'account.updatePasswordCardDescription'} />
|
||||
<Trans i18nKey={'account:updatePasswordCardDescription'} />
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
@@ -143,11 +144,11 @@ export function PersonalAccountSettingsContainer(
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
<Trans i18nKey={'account.multiFactorAuth'} />
|
||||
<Trans i18nKey={'account:multiFactorAuth'} />
|
||||
</CardTitle>
|
||||
|
||||
<CardDescription>
|
||||
<Trans i18nKey={'account.multiFactorAuthDescription'} />
|
||||
<Trans i18nKey={'account:multiFactorAuthDescription'} />
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
@@ -159,11 +160,11 @@ export function PersonalAccountSettingsContainer(
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
<Trans i18nKey={'account.linkedAccounts'} />
|
||||
<Trans i18nKey={'account:linkedAccounts'} />
|
||||
</CardTitle>
|
||||
|
||||
<CardDescription>
|
||||
<Trans i18nKey={'account.linkedAccountsDescription'} />
|
||||
<Trans i18nKey={'account:linkedAccountsDescription'} />
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
@@ -182,11 +183,11 @@ export function PersonalAccountSettingsContainer(
|
||||
<Card className={'border-destructive'}>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
<Trans i18nKey={'account.dangerZone'} />
|
||||
<Trans i18nKey={'account:dangerZone'} />
|
||||
</CardTitle>
|
||||
|
||||
<CardDescription>
|
||||
<Trans i18nKey={'account.dangerZoneDescription'} />
|
||||
<Trans i18nKey={'account:dangerZoneDescription'} />
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
@@ -200,7 +201,10 @@ export function PersonalAccountSettingsContainer(
|
||||
}
|
||||
|
||||
function useSupportMultiLanguage() {
|
||||
const { locales } = routing;
|
||||
const { i18n } = useTranslation();
|
||||
const langs = (i18n?.options?.supportedLngs as string[]) ?? [];
|
||||
|
||||
return locales.length > 1;
|
||||
const supportedLangs = langs.filter((lang) => lang !== 'cimode');
|
||||
|
||||
return supportedLangs.length > 1;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Check, Mail } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { CheckIcon } from '@radix-ui/react-icons';
|
||||
import { Mail } from 'lucide-react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useUpdateUser } from '@kit/supabase/hooks/use-update-user-mutation';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||
@@ -61,7 +62,7 @@ export function UpdateEmailForm({
|
||||
callbackPath: string;
|
||||
onSuccess?: () => void;
|
||||
}) {
|
||||
const t = useTranslations('account');
|
||||
const { t } = useTranslation('account');
|
||||
const updateUserMutation = useUpdateUser();
|
||||
const isSettingEmail = !email;
|
||||
|
||||
@@ -107,14 +108,14 @@ export function UpdateEmailForm({
|
||||
>
|
||||
<If condition={updateUserMutation.data}>
|
||||
<Alert variant={'success'}>
|
||||
<Check className={'h-4'} />
|
||||
<CheckIcon className={'h-4'} />
|
||||
|
||||
<AlertTitle>
|
||||
<Trans
|
||||
i18nKey={
|
||||
isSettingEmail
|
||||
? 'account.setEmailSuccess'
|
||||
: 'account.updateEmailSuccess'
|
||||
? 'account:setEmailSuccess'
|
||||
: 'account:updateEmailSuccess'
|
||||
}
|
||||
/>
|
||||
</AlertTitle>
|
||||
@@ -123,8 +124,8 @@ export function UpdateEmailForm({
|
||||
<Trans
|
||||
i18nKey={
|
||||
isSettingEmail
|
||||
? 'account.setEmailSuccessMessage'
|
||||
: 'account.updateEmailSuccessMessage'
|
||||
? 'account:setEmailSuccessMessage'
|
||||
: 'account:updateEmailSuccessMessage'
|
||||
}
|
||||
/>
|
||||
</AlertDescription>
|
||||
@@ -147,7 +148,9 @@ export function UpdateEmailForm({
|
||||
required
|
||||
type={'email'}
|
||||
placeholder={t(
|
||||
isSettingEmail ? 'emailAddress' : 'newEmail',
|
||||
isSettingEmail
|
||||
? 'account:emailAddress'
|
||||
: 'account:newEmail',
|
||||
)}
|
||||
{...field}
|
||||
/>
|
||||
@@ -159,7 +162,7 @@ export function UpdateEmailForm({
|
||||
)}
|
||||
name={'email'}
|
||||
/>
|
||||
Perform
|
||||
|
||||
<FormField
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
@@ -174,7 +177,7 @@ export function UpdateEmailForm({
|
||||
data-test={'account-email-form-repeat-email-input'}
|
||||
required
|
||||
type={'email'}
|
||||
placeholder={t('repeatEmail')}
|
||||
placeholder={t('account:repeatEmail')}
|
||||
/>
|
||||
</InputGroup>
|
||||
</FormControl>
|
||||
@@ -187,12 +190,12 @@ export function UpdateEmailForm({
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button type="submit" disabled={updateUserMutation.isPending}>
|
||||
<Button disabled={updateUserMutation.isPending}>
|
||||
<Trans
|
||||
i18nKey={
|
||||
isSettingEmail
|
||||
? 'account.setEmailAddress'
|
||||
: 'account.updateEmailSubmitLabel'
|
||||
? 'account:setEmailAddress'
|
||||
: 'account:updateEmailSubmitLabel'
|
||||
}
|
||||
/>
|
||||
</Button>
|
||||
|
||||
@@ -112,9 +112,9 @@ export function LinkAccountsList(props: LinkAccountsListProps) {
|
||||
const promise = unlinkMutation.mutateAsync(identity);
|
||||
|
||||
toast.promise(promise, {
|
||||
loading: <Trans i18nKey={'account.unlinkingAccount'} />,
|
||||
success: <Trans i18nKey={'account.accountUnlinked'} />,
|
||||
error: <Trans i18nKey={'account.unlinkAccountError'} />,
|
||||
loading: <Trans i18nKey={'account:unlinkingAccount'} />,
|
||||
success: <Trans i18nKey={'account:accountUnlinked'} />,
|
||||
error: <Trans i18nKey={'account:unlinkAccountError'} />,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -129,9 +129,9 @@ export function LinkAccountsList(props: LinkAccountsListProps) {
|
||||
});
|
||||
|
||||
toast.promise(promise, {
|
||||
loading: <Trans i18nKey={'account.linkingAccount'} />,
|
||||
success: <Trans i18nKey={'account.accountLinked'} />,
|
||||
error: <Trans i18nKey={'account.linkAccountError'} />,
|
||||
loading: <Trans i18nKey={'account:linkingAccount'} />,
|
||||
success: <Trans i18nKey={'account:accountLinked'} />,
|
||||
error: <Trans i18nKey={'account:linkAccountError'} />,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -149,11 +149,11 @@ export function LinkAccountsList(props: LinkAccountsListProps) {
|
||||
<div className="space-y-2.5">
|
||||
<div>
|
||||
<h3 className="text-foreground text-sm font-medium">
|
||||
<Trans i18nKey={'account.linkedMethods'} />
|
||||
<Trans i18nKey={'account:linkedMethods'} />
|
||||
</h3>
|
||||
|
||||
<p className="text-muted-foreground text-xs">
|
||||
<Trans i18nKey={'account.alreadyLinkedMethodsDescription'} />
|
||||
<Trans i18nKey={'account:alreadyLinkedMethodsDescription'} />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -185,30 +185,28 @@ export function LinkAccountsList(props: LinkAccountsListProps) {
|
||||
<ItemActions>
|
||||
<If condition={hasMultipleIdentities}>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger
|
||||
render={
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={unlinkMutation.isPending}
|
||||
>
|
||||
<If condition={unlinkMutation.isPending}>
|
||||
<Spinner className="mr-2 h-3 w-3" />
|
||||
</If>
|
||||
<Trans i18nKey={'account.unlinkAccount'} />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={unlinkMutation.isPending}
|
||||
>
|
||||
<If condition={unlinkMutation.isPending}>
|
||||
<Spinner className="mr-2 h-3 w-3" />
|
||||
</If>
|
||||
<Trans i18nKey={'account:unlinkAccount'} />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
<Trans i18nKey={'account.confirmUnlinkAccount'} />
|
||||
<Trans i18nKey={'account:confirmUnlinkAccount'} />
|
||||
</AlertDialogTitle>
|
||||
|
||||
<AlertDialogDescription>
|
||||
<Trans
|
||||
i18nKey={'account.unlinkAccountConfirmation'}
|
||||
i18nKey={'account:unlinkAccountConfirmation'}
|
||||
values={{ provider: identity.provider }}
|
||||
/>
|
||||
</AlertDialogDescription>
|
||||
@@ -216,14 +214,14 @@ export function LinkAccountsList(props: LinkAccountsListProps) {
|
||||
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>
|
||||
<Trans i18nKey={'common.cancel'} />
|
||||
<Trans i18nKey={'common:cancel'} />
|
||||
</AlertDialogCancel>
|
||||
|
||||
<AlertDialogAction
|
||||
onClick={() => handleUnlinkAccount(identity)}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
<Trans i18nKey={'account.unlinkAccount'} />
|
||||
<Trans i18nKey={'account:unlinkAccount'} />
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
@@ -245,11 +243,11 @@ export function LinkAccountsList(props: LinkAccountsListProps) {
|
||||
<div className="space-y-2.5">
|
||||
<div>
|
||||
<h3 className="text-foreground text-sm font-medium">
|
||||
<Trans i18nKey={'account.availableMethods'} />
|
||||
<Trans i18nKey={'account:availableMethods'} />
|
||||
</h3>
|
||||
|
||||
<p className="text-muted-foreground text-xs">
|
||||
<Trans i18nKey={'account.availableMethodsDescription'} />
|
||||
<Trans i18nKey={'account:availableMethodsDescription'} />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -283,7 +281,7 @@ export function LinkAccountsList(props: LinkAccountsListProps) {
|
||||
|
||||
<ItemDescription>
|
||||
<Trans
|
||||
i18nKey={'account.linkAccountDescription'}
|
||||
i18nKey={'account:linkAccountDescription'}
|
||||
values={{ provider }}
|
||||
/>
|
||||
</ItemDescription>
|
||||
@@ -301,7 +299,7 @@ function NoAccountsAvailable() {
|
||||
return (
|
||||
<div>
|
||||
<span className="text-muted-foreground text-xs">
|
||||
<Trans i18nKey={'account.noAccountsAvailable'} />
|
||||
<Trans i18nKey={'account:noAccountsAvailable'} />
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
@@ -312,41 +310,38 @@ function UpdateEmailDialog(props: { redirectTo: string }) {
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger
|
||||
nativeButton={false}
|
||||
render={
|
||||
<Item variant="outline" role="button" className="hover:bg-muted/50">
|
||||
<ItemMedia>
|
||||
<div className="text-muted-foreground flex h-5 w-5 items-center justify-center">
|
||||
<OauthProviderLogoImage providerId={'email'} />
|
||||
<DialogTrigger asChild>
|
||||
<Item variant="outline" role="button" className="hover:bg-muted/50">
|
||||
<ItemMedia>
|
||||
<div className="text-muted-foreground flex h-5 w-5 items-center justify-center">
|
||||
<OauthProviderLogoImage providerId={'email'} />
|
||||
</div>
|
||||
</ItemMedia>
|
||||
|
||||
<ItemContent>
|
||||
<ItemHeader>
|
||||
<div className="flex flex-col">
|
||||
<ItemTitle className="text-sm font-medium">
|
||||
<Trans i18nKey={'account:setEmailAddress'} />
|
||||
</ItemTitle>
|
||||
|
||||
<ItemDescription>
|
||||
<Trans i18nKey={'account:setEmailDescription'} />
|
||||
</ItemDescription>
|
||||
</div>
|
||||
</ItemMedia>
|
||||
|
||||
<ItemContent>
|
||||
<ItemHeader>
|
||||
<div className="flex flex-col">
|
||||
<ItemTitle className="text-sm font-medium">
|
||||
<Trans i18nKey={'account.setEmailAddress'} />
|
||||
</ItemTitle>
|
||||
|
||||
<ItemDescription>
|
||||
<Trans i18nKey={'account.setEmailDescription'} />
|
||||
</ItemDescription>
|
||||
</div>
|
||||
</ItemHeader>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
}
|
||||
/>
|
||||
</ItemHeader>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<Trans i18nKey={'account.setEmailAddress'} />
|
||||
<Trans i18nKey={'account:setEmailAddress'} />
|
||||
</DialogTitle>
|
||||
|
||||
<DialogDescription>
|
||||
<Trans i18nKey={'account.setEmailDescription'} />
|
||||
<Trans i18nKey={'account:setEmailDescription'} />
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -378,38 +373,34 @@ function UpdatePasswordDialog(props: {
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger
|
||||
nativeButton={false}
|
||||
data-test="open-password-dialog-trigger"
|
||||
render={
|
||||
<Item variant="outline" role="button" className="hover:bg-muted/50">
|
||||
<ItemMedia>
|
||||
<div className="text-muted-foreground flex h-5 w-5 items-center justify-center">
|
||||
<OauthProviderLogoImage providerId={'password'} />
|
||||
<DialogTrigger asChild data-test="open-password-dialog-trigger">
|
||||
<Item variant="outline" role="button" className="hover:bg-muted/50">
|
||||
<ItemMedia>
|
||||
<div className="text-muted-foreground flex h-5 w-5 items-center justify-center">
|
||||
<OauthProviderLogoImage providerId={'password'} />
|
||||
</div>
|
||||
</ItemMedia>
|
||||
|
||||
<ItemContent>
|
||||
<ItemHeader>
|
||||
<div className="flex flex-col">
|
||||
<ItemTitle className="text-sm font-medium">
|
||||
<Trans i18nKey={'account:linkEmailPassword'} />
|
||||
</ItemTitle>
|
||||
|
||||
<ItemDescription>
|
||||
<Trans i18nKey={'account:updatePasswordDescription'} />
|
||||
</ItemDescription>
|
||||
</div>
|
||||
</ItemMedia>
|
||||
|
||||
<ItemContent>
|
||||
<ItemHeader>
|
||||
<div className="flex flex-col">
|
||||
<ItemTitle className="text-sm font-medium">
|
||||
<Trans i18nKey={'account.linkEmailPassword'} />
|
||||
</ItemTitle>
|
||||
|
||||
<ItemDescription>
|
||||
<Trans i18nKey={'account.updatePasswordDescription'} />
|
||||
</ItemDescription>
|
||||
</div>
|
||||
</ItemHeader>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
}
|
||||
/>
|
||||
</ItemHeader>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<Trans i18nKey={'account.linkEmailPassword'} />
|
||||
<Trans i18nKey={'account:linkEmailPassword'} />
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
|
||||
@@ -4,9 +4,10 @@ import { useCallback, useState } from 'react';
|
||||
|
||||
import type { Factor } from '@supabase/supabase-js';
|
||||
|
||||
import { ExclamationTriangleIcon } from '@radix-ui/react-icons';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { ShieldCheck, TriangleAlert, X } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { ShieldCheck, X } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useFetchAuthFactors } from '@kit/supabase/hooks/use-fetch-mfa-factors';
|
||||
import { useSupabase } from '@kit/supabase/hooks/use-supabase';
|
||||
@@ -77,7 +78,7 @@ function FactorsTableContainer(props: { userId: string }) {
|
||||
<Spinner />
|
||||
|
||||
<div>
|
||||
<Trans i18nKey={'account.loadingFactors'} />
|
||||
<Trans i18nKey={'account:loadingFactors'} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -87,14 +88,14 @@ function FactorsTableContainer(props: { userId: string }) {
|
||||
return (
|
||||
<div>
|
||||
<Alert variant={'destructive'}>
|
||||
<TriangleAlert className={'h-4'} />
|
||||
<ExclamationTriangleIcon className={'h-4'} />
|
||||
|
||||
<AlertTitle>
|
||||
<Trans i18nKey={'account.factorsListError'} />
|
||||
<Trans i18nKey={'account:factorsListError'} />
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
<Trans i18nKey={'account.factorsListErrorDescription'} />
|
||||
<Trans i18nKey={'account:factorsListErrorDescription'} />
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
@@ -113,11 +114,11 @@ function FactorsTableContainer(props: { userId: string }) {
|
||||
|
||||
<ItemContent>
|
||||
<ItemTitle>
|
||||
<Trans i18nKey={'account.multiFactorAuthHeading'} />
|
||||
<Trans i18nKey={'account:multiFactorAuthHeading'} />
|
||||
</ItemTitle>
|
||||
|
||||
<ItemDescription>
|
||||
<Trans i18nKey={'account.multiFactorAuthDescription'} />
|
||||
<Trans i18nKey={'account:multiFactorAuthDescription'} />
|
||||
</ItemDescription>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
@@ -135,7 +136,7 @@ function ConfirmUnenrollFactorModal(
|
||||
setIsModalOpen: (isOpen: boolean) => void;
|
||||
}>,
|
||||
) {
|
||||
const t = useTranslations();
|
||||
const { t } = useTranslation();
|
||||
const unEnroll = useUnenrollFactor(props.userId);
|
||||
|
||||
const onUnenrollRequested = useCallback(
|
||||
@@ -148,18 +149,15 @@ function ConfirmUnenrollFactorModal(
|
||||
if (!response.success) {
|
||||
const errorCode = response.data;
|
||||
|
||||
throw t(
|
||||
`auth.errors.${errorCode}` as never,
|
||||
{
|
||||
defaultValue: t(`account.unenrollFactorError` as never),
|
||||
} as never,
|
||||
);
|
||||
throw t(`auth:errors.${errorCode}`, {
|
||||
defaultValue: t(`account:unenrollFactorError`),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
toast.promise(promise, {
|
||||
loading: t(`account.unenrollingFactor` as never),
|
||||
success: t(`account.unenrollFactorSuccess` as never),
|
||||
loading: t(`account:unenrollingFactor`),
|
||||
success: t(`account:unenrollFactorSuccess`),
|
||||
error: (error: string) => {
|
||||
return error;
|
||||
},
|
||||
@@ -173,17 +171,17 @@ function ConfirmUnenrollFactorModal(
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
<Trans i18nKey={'account.unenrollFactorModalHeading'} />
|
||||
<Trans i18nKey={'account:unenrollFactorModalHeading'} />
|
||||
</AlertDialogTitle>
|
||||
|
||||
<AlertDialogDescription>
|
||||
<Trans i18nKey={'account.unenrollFactorModalDescription'} />
|
||||
<Trans i18nKey={'account:unenrollFactorModalDescription'} />
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>
|
||||
<Trans i18nKey={'common.cancel'} />
|
||||
<Trans i18nKey={'common:cancel'} />
|
||||
</AlertDialogCancel>
|
||||
|
||||
<AlertDialogAction
|
||||
@@ -191,7 +189,7 @@ function ConfirmUnenrollFactorModal(
|
||||
disabled={unEnroll.isPending}
|
||||
onClick={() => onUnenrollRequested(props.factorId)}
|
||||
>
|
||||
<Trans i18nKey={'account.unenrollFactorModalButtonLabel'} />
|
||||
<Trans i18nKey={'account:unenrollFactorModalButtonLabel'} />
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
@@ -214,13 +212,13 @@ function FactorsTable({
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>
|
||||
<Trans i18nKey={'account.factorName'} />
|
||||
<Trans i18nKey={'account:factorName'} />
|
||||
</TableHead>
|
||||
<TableHead>
|
||||
<Trans i18nKey={'account.factorType'} />
|
||||
<Trans i18nKey={'account:factorType'} />
|
||||
</TableHead>
|
||||
<TableHead>
|
||||
<Trans i18nKey={'account.factorStatus'} />
|
||||
<Trans i18nKey={'account:factorStatus'} />
|
||||
</TableHead>
|
||||
|
||||
<TableHead />
|
||||
@@ -252,20 +250,18 @@ function FactorsTable({
|
||||
<td className={'flex justify-end'}>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<Button
|
||||
variant={'ghost'}
|
||||
size={'icon'}
|
||||
onClick={() => setUnenrolling(factor.id)}
|
||||
>
|
||||
<X className={'h-4'} />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant={'ghost'}
|
||||
size={'icon'}
|
||||
onClick={() => setUnenrolling(factor.id)}
|
||||
>
|
||||
<X className={'h-4'} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent>
|
||||
<Trans i18nKey={'account.unenrollTooltip'} />
|
||||
<Trans i18nKey={'account:unenrollTooltip'} />
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { ExclamationTriangleIcon } from '@radix-ui/react-icons';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { ArrowLeftIcon, TriangleAlert } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { ArrowLeftIcon } from 'lucide-react';
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
import * as z from 'zod';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { useSupabase } from '@kit/supabase/hooks/use-supabase';
|
||||
import { useFactorsMutationKey } from '@kit/supabase/hooks/use-user-factors-mutation-key';
|
||||
@@ -44,33 +45,34 @@ import { Trans } from '@kit/ui/trans';
|
||||
import { refreshAuthSession } from '../../../server/personal-accounts-server-actions';
|
||||
|
||||
export function MultiFactorAuthSetupDialog(props: { userId: string }) {
|
||||
const t = useTranslations();
|
||||
const { t } = useTranslation();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const onEnrollSuccess = useCallback(() => {
|
||||
setIsOpen(false);
|
||||
|
||||
return toast.success(t(`account.multiFactorSetupSuccess` as never));
|
||||
return toast.success(t(`account:multiFactorSetupSuccess`));
|
||||
}, [t]);
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen} disablePointerDismissal>
|
||||
<DialogTrigger
|
||||
render={
|
||||
<Button>
|
||||
<Trans i18nKey={'account.setupMfaButtonLabel'} />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>
|
||||
<Trans i18nKey={'account:setupMfaButtonLabel'} />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent>
|
||||
<DialogContent
|
||||
onInteractOutside={(e) => e.preventDefault()}
|
||||
onEscapeKeyDown={(e) => e.preventDefault()}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<Trans i18nKey={'account.setupMfaButtonLabel'} />
|
||||
<Trans i18nKey={'account:setupMfaButtonLabel'} />
|
||||
</DialogTitle>
|
||||
|
||||
<DialogDescription>
|
||||
<Trans i18nKey={'account.multiFactorAuthDescription'} />
|
||||
<Trans i18nKey={'account:multiFactorAuthDescription'} />
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -208,7 +210,7 @@ function MultiFactorAuthSetupForm({
|
||||
|
||||
<FormDescription>
|
||||
<Trans
|
||||
i18nKey={'account.verifyActivationCodeDescription'}
|
||||
i18nKey={'account:verifyActivationCodeDescription'}
|
||||
/>
|
||||
</FormDescription>
|
||||
|
||||
@@ -221,7 +223,7 @@ function MultiFactorAuthSetupForm({
|
||||
|
||||
<div className={'flex justify-end space-x-2'}>
|
||||
<Button type={'button'} variant={'ghost'} onClick={onCancel}>
|
||||
<Trans i18nKey={'common.cancel'} />
|
||||
<Trans i18nKey={'common:cancel'} />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
@@ -231,9 +233,9 @@ function MultiFactorAuthSetupForm({
|
||||
type={'submit'}
|
||||
>
|
||||
{state.loading ? (
|
||||
<Trans i18nKey={'account.verifyingCode'} />
|
||||
<Trans i18nKey={'account:verifyingCode'} />
|
||||
) : (
|
||||
<Trans i18nKey={'account.enableMfaFactor'} />
|
||||
<Trans i18nKey={'account:enableMfaFactor'} />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -255,7 +257,7 @@ function FactorQrCode({
|
||||
onSetFactorId: (factorId: string) => void;
|
||||
}>) {
|
||||
const enrollFactorMutation = useEnrollFactor(userId);
|
||||
const t = useTranslations();
|
||||
const { t } = useTranslation();
|
||||
const [error, setError] = useState<string>('');
|
||||
|
||||
const form = useForm({
|
||||
@@ -277,16 +279,16 @@ function FactorQrCode({
|
||||
return (
|
||||
<div className={'flex w-full flex-col space-y-2'}>
|
||||
<Alert variant={'destructive'}>
|
||||
<TriangleAlert className={'h-4'} />
|
||||
<ExclamationTriangleIcon className={'h-4'} />
|
||||
|
||||
<AlertTitle>
|
||||
<Trans i18nKey={'account.qrCodeErrorHeading'} />
|
||||
<Trans i18nKey={'account:qrCodeErrorHeading'} />
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
<Trans
|
||||
i18nKey={`auth.errors.${error}`}
|
||||
defaults={t('account.qrCodeErrorDescription')}
|
||||
i18nKey={`auth:errors.${error}`}
|
||||
defaults={t('account:qrCodeErrorDescription')}
|
||||
/>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
@@ -294,7 +296,7 @@ function FactorQrCode({
|
||||
<div>
|
||||
<Button variant={'outline'} onClick={onCancel}>
|
||||
<ArrowLeftIcon className={'h-4'} />
|
||||
<Trans i18nKey={`common.retry`} />
|
||||
<Trans i18nKey={`common:retry`} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -334,7 +336,7 @@ function FactorQrCode({
|
||||
>
|
||||
<p>
|
||||
<span className={'text-muted-foreground text-sm'}>
|
||||
<Trans i18nKey={'account.multiFactorModalHeading'} />
|
||||
<Trans i18nKey={'account:multiFactorModalHeading'} />
|
||||
</span>
|
||||
</p>
|
||||
|
||||
@@ -377,7 +379,7 @@ function FactorNameForm(
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans i18nKey={'account.factorNameLabel'} />
|
||||
<Trans i18nKey={'account:factorNameLabel'} />
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
@@ -385,7 +387,7 @@ function FactorNameForm(
|
||||
</FormControl>
|
||||
|
||||
<FormDescription>
|
||||
<Trans i18nKey={'account.factorNameHint'} />
|
||||
<Trans i18nKey={'account:factorNameHint'} />
|
||||
</FormDescription>
|
||||
|
||||
<FormMessage />
|
||||
@@ -396,11 +398,11 @@ function FactorNameForm(
|
||||
|
||||
<div className={'flex justify-end space-x-2'}>
|
||||
<Button type={'button'} variant={'ghost'} onClick={props.onCancel}>
|
||||
<Trans i18nKey={'common.cancel'} />
|
||||
<Trans i18nKey={'common:cancel'} />
|
||||
</Button>
|
||||
|
||||
<Button type={'submit'}>
|
||||
<Trans i18nKey={'account.factorNameSubmitLabel'} />
|
||||
<Trans i18nKey={'account:factorNameSubmitLabel'} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -499,14 +501,14 @@ function useVerifyCodeMutation(userId: string) {
|
||||
function ErrorAlert() {
|
||||
return (
|
||||
<Alert variant={'destructive'}>
|
||||
<TriangleAlert className={'h-4'} />
|
||||
<ExclamationTriangleIcon className={'h-4'} />
|
||||
|
||||
<AlertTitle>
|
||||
<Trans i18nKey={'account.multiFactorSetupErrorHeading'} />
|
||||
<Trans i18nKey={'account:multiFactorSetupErrorHeading'} />
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
<Trans i18nKey={'account.multiFactorSetupErrorDescription'} />
|
||||
<Trans i18nKey={'account:multiFactorSetupErrorDescription'} />
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
|
||||
@@ -5,9 +5,10 @@ import { useState } from 'react';
|
||||
import type { PostgrestError } from '@supabase/supabase-js';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Check, Lock, TriangleAlert, XIcon } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { ExclamationTriangleIcon } from '@radix-ui/react-icons';
|
||||
import { Check, Lock, XIcon } from 'lucide-react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useUpdateUser } from '@kit/supabase/hooks/use-update-user-mutation';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||
@@ -40,7 +41,7 @@ export const UpdatePasswordForm = ({
|
||||
callbackPath: string;
|
||||
onSuccess?: () => void;
|
||||
}) => {
|
||||
const t = useTranslations('account');
|
||||
const { t } = useTranslation('account');
|
||||
const updateUserMutation = useUpdateUser();
|
||||
const [needsReauthentication, setNeedsReauthentication] = useState(false);
|
||||
|
||||
@@ -130,7 +131,7 @@ export const UpdatePasswordForm = ({
|
||||
autoComplete={'new-password'}
|
||||
required
|
||||
type={'password'}
|
||||
placeholder={t('newPassword')}
|
||||
placeholder={t('account:newPassword')}
|
||||
{...field}
|
||||
/>
|
||||
</InputGroup>
|
||||
@@ -159,14 +160,14 @@ export const UpdatePasswordForm = ({
|
||||
}
|
||||
required
|
||||
type={'password'}
|
||||
placeholder={t('repeatPassword')}
|
||||
placeholder={t('account:repeatPassword')}
|
||||
{...field}
|
||||
/>
|
||||
</InputGroup>
|
||||
</FormControl>
|
||||
|
||||
<FormDescription>
|
||||
<Trans i18nKey={'account.repeatPasswordDescription'} />
|
||||
<Trans i18nKey={'account:repeatPasswordDescription'} />
|
||||
</FormDescription>
|
||||
|
||||
<FormMessage />
|
||||
@@ -178,11 +179,10 @@ export const UpdatePasswordForm = ({
|
||||
|
||||
<div>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={updateUserMutation.isPending}
|
||||
data-test="identity-form-submit"
|
||||
>
|
||||
<Trans i18nKey={'account.updatePasswordSubmitLabel'} />
|
||||
<Trans i18nKey={'account:updatePasswordSubmitLabel'} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -192,20 +192,20 @@ export const UpdatePasswordForm = ({
|
||||
};
|
||||
|
||||
function ErrorAlert({ error }: { error: { code: string } }) {
|
||||
const t = useTranslations();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Alert variant={'destructive'}>
|
||||
<XIcon className={'h-4'} />
|
||||
|
||||
<AlertTitle>
|
||||
<Trans i18nKey={'account.updatePasswordError'} />
|
||||
<Trans i18nKey={'account:updatePasswordError'} />
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
<Trans
|
||||
i18nKey={`auth.errors.${error.code}`}
|
||||
defaults={t('auth.resetPasswordError')}
|
||||
i18nKey={`auth:errors.${error.code}`}
|
||||
defaults={t('auth:resetPasswordError')}
|
||||
/>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
@@ -218,11 +218,11 @@ function SuccessAlert() {
|
||||
<Check className={'h-4'} />
|
||||
|
||||
<AlertTitle>
|
||||
<Trans i18nKey={'account.updatePasswordSuccess'} />
|
||||
<Trans i18nKey={'account:updatePasswordSuccess'} />
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
<Trans i18nKey={'account.updatePasswordSuccessMessage'} />
|
||||
<Trans i18nKey={'account:updatePasswordSuccessMessage'} />
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
@@ -231,14 +231,14 @@ function SuccessAlert() {
|
||||
function NeedsReauthenticationAlert() {
|
||||
return (
|
||||
<Alert variant={'warning'}>
|
||||
<TriangleAlert className={'h-4'} />
|
||||
<ExclamationTriangleIcon className={'h-4'} />
|
||||
|
||||
<AlertTitle>
|
||||
<Trans i18nKey={'account.needsReauthentication'} />
|
||||
<Trans i18nKey={'account:needsReauthentication'} />
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
<Trans i18nKey={'account.needsReauthenticationDescription'} />
|
||||
<Trans i18nKey={'account:needsReauthenticationDescription'} />
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { User } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Database } from '@kit/supabase/database';
|
||||
import { Button } from '@kit/ui/button';
|
||||
@@ -35,7 +35,7 @@ export function UpdateAccountDetailsForm({
|
||||
onUpdate: (user: Partial<UpdateUserDataParams>) => void;
|
||||
}) {
|
||||
const updateAccountMutation = useUpdateAccountData(userId);
|
||||
const t = useTranslations('account');
|
||||
const { t } = useTranslation('account');
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(AccountDetailsSchema),
|
||||
@@ -79,7 +79,7 @@ export function UpdateAccountDetailsForm({
|
||||
<InputGroupInput
|
||||
data-test={'account-display-name'}
|
||||
minLength={2}
|
||||
placeholder={t('name')}
|
||||
placeholder={t('account:name')}
|
||||
maxLength={100}
|
||||
{...field}
|
||||
/>
|
||||
@@ -92,8 +92,8 @@ export function UpdateAccountDetailsForm({
|
||||
/>
|
||||
|
||||
<div>
|
||||
<Button type="submit" disabled={updateAccountMutation.isPending}>
|
||||
<Trans i18nKey={'account.updateProfileSubmitLabel'} />
|
||||
<Button disabled={updateAccountMutation.isPending}>
|
||||
<Trans i18nKey={'account:updateProfileSubmitLabel'} />
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useCallback } from 'react';
|
||||
|
||||
import type { SupabaseClient } from '@supabase/supabase-js';
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Database } from '@kit/supabase/database';
|
||||
import { useSupabase } from '@kit/supabase/hooks/use-supabase';
|
||||
@@ -41,7 +41,7 @@ function UploadProfileAvatarForm(props: {
|
||||
onAvatarUpdated: () => void;
|
||||
}) {
|
||||
const client = useSupabase();
|
||||
const t = useTranslations('account');
|
||||
const { t } = useTranslation('account');
|
||||
|
||||
const createToaster = useCallback(
|
||||
(promise: () => Promise<unknown>) => {
|
||||
@@ -111,11 +111,11 @@ function UploadProfileAvatarForm(props: {
|
||||
<ImageUploader value={props.pictureUrl} onValueChange={onValueChange}>
|
||||
<div className={'flex flex-col space-y-1'}>
|
||||
<span className={'text-sm'}>
|
||||
<Trans i18nKey={'account.profilePictureHeading'} />
|
||||
<Trans i18nKey={'account:profilePictureHeading'} />
|
||||
</span>
|
||||
|
||||
<span className={'text-xs'}>
|
||||
<Trans i18nKey={'account.profilePictureSubheading'} />
|
||||
<Trans i18nKey={'account:profilePictureSubheading'} />
|
||||
</span>
|
||||
</div>
|
||||
</ImageUploader>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const AccountDetailsSchema = z.object({
|
||||
displayName: z.string().min(2).max(100),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const DeletePersonalAccountSchema = z.object({
|
||||
otp: z.string().min(6),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const LinkEmailPasswordSchema = z
|
||||
.object({
|
||||
@@ -8,5 +8,5 @@ export const LinkEmailPasswordSchema = z
|
||||
})
|
||||
.refine((values) => values.password === values.repeatPassword, {
|
||||
path: ['repeatPassword'],
|
||||
message: `account.passwordNotMatching`,
|
||||
message: `account:passwordNotMatching`,
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const UpdateEmailSchema = {
|
||||
withTranslation: (errorMessage: string) => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const PasswordUpdateSchema = {
|
||||
withTranslation: (errorMessage: string) => {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
import { authActionClient } from '@kit/next/safe-action';
|
||||
import { enhanceAction } from '@kit/next/actions';
|
||||
import { createOtpApi } from '@kit/otp';
|
||||
import { getLogger } from '@kit/shared/logger';
|
||||
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
|
||||
@@ -23,17 +23,25 @@ export async function refreshAuthSession() {
|
||||
return {};
|
||||
}
|
||||
|
||||
export const deletePersonalAccountAction = authActionClient
|
||||
.schema(DeletePersonalAccountSchema)
|
||||
.action(async ({ parsedInput: data, ctx: { user } }) => {
|
||||
export const deletePersonalAccountAction = enhanceAction(
|
||||
async (formData: FormData, user) => {
|
||||
const logger = await getLogger();
|
||||
|
||||
// validate the form data
|
||||
const { success } = DeletePersonalAccountSchema.safeParse(
|
||||
Object.fromEntries(formData.entries()),
|
||||
);
|
||||
|
||||
if (!success) {
|
||||
throw new Error('Invalid form data');
|
||||
}
|
||||
|
||||
const ctx = {
|
||||
name: 'account.delete',
|
||||
userId: user.id,
|
||||
};
|
||||
|
||||
const otp = data.otp;
|
||||
const otp = formData.get('otp') as string;
|
||||
|
||||
if (!otp) {
|
||||
throw new Error('OTP is required');
|
||||
@@ -93,4 +101,6 @@ export const deletePersonalAccountAction = authActionClient
|
||||
|
||||
// redirect to the home page
|
||||
redirect('/');
|
||||
});
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
@@ -2,7 +2,7 @@ import 'server-only';
|
||||
|
||||
import { SupabaseClient } from '@supabase/supabase-js';
|
||||
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { getLogger } from '@kit/shared/logger';
|
||||
import { Database } from '@kit/supabase/database';
|
||||
@@ -133,12 +133,12 @@ class DeletePersonalAccountService {
|
||||
.object({
|
||||
productName: z
|
||||
.string({
|
||||
error: 'NEXT_PUBLIC_PRODUCT_NAME is required',
|
||||
required_error: 'NEXT_PUBLIC_PRODUCT_NAME is required',
|
||||
})
|
||||
.min(1),
|
||||
fromEmail: z
|
||||
.string({
|
||||
error: 'EMAIL_SENDER is required',
|
||||
required_error: 'EMAIL_SENDER is required',
|
||||
})
|
||||
.min(1),
|
||||
})
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
"@types/react": "catalog:",
|
||||
"lucide-react": "catalog:",
|
||||
"next": "catalog:",
|
||||
"next-safe-action": "catalog:",
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
"react-hook-form": "catalog:",
|
||||
|
||||
@@ -7,7 +7,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import { EllipsisVertical } from 'lucide-react';
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { Tables } from '@kit/supabase/database';
|
||||
import { Button } from '@kit/ui/button';
|
||||
@@ -77,7 +77,7 @@ export function AdminAccountsTable(
|
||||
}
|
||||
|
||||
function AccountsTableFilters(props: {
|
||||
filters: z.output<typeof FiltersSchema>;
|
||||
filters: z.infer<typeof FiltersSchema>;
|
||||
}) {
|
||||
const form = useForm({
|
||||
resolver: zodResolver(FiltersSchema),
|
||||
@@ -92,7 +92,7 @@ function AccountsTableFilters(props: {
|
||||
const router = useRouter();
|
||||
const pathName = usePathname();
|
||||
|
||||
const onSubmit = ({ type, query }: z.output<typeof FiltersSchema>) => {
|
||||
const onSubmit = ({ type, query }: z.infer<typeof FiltersSchema>) => {
|
||||
const params = new URLSearchParams({
|
||||
account_type: type,
|
||||
query: query ?? '',
|
||||
@@ -105,12 +105,6 @@ function AccountsTableFilters(props: {
|
||||
|
||||
const type = useWatch({ control: form.control, name: 'type' });
|
||||
|
||||
const options = {
|
||||
all: 'All Accounts',
|
||||
team: 'Team',
|
||||
personal: 'Personal',
|
||||
};
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
@@ -122,7 +116,7 @@ function AccountsTableFilters(props: {
|
||||
onValueChange={(value) => {
|
||||
form.setValue(
|
||||
'type',
|
||||
value as z.output<typeof FiltersSchema>['type'],
|
||||
value as z.infer<typeof FiltersSchema>['type'],
|
||||
{
|
||||
shouldValidate: true,
|
||||
shouldDirty: true,
|
||||
@@ -134,20 +128,16 @@ function AccountsTableFilters(props: {
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={'Account Type'}>
|
||||
{(value: keyof typeof options) => options[value]}
|
||||
</SelectValue>
|
||||
<SelectValue placeholder={'Account Type'} />
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectLabel>Account Type</SelectLabel>
|
||||
|
||||
{Object.entries(options).map(([key, value]) => (
|
||||
<SelectItem key={key} value={key}>
|
||||
{value}
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectItem value={'all'}>All accounts</SelectItem>
|
||||
<SelectItem value={'team'}>Team</SelectItem>
|
||||
<SelectItem value={'personal'}>Personal</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
@@ -167,8 +157,6 @@ function AccountsTableFilters(props: {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<button type="submit" hidden />
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
@@ -223,13 +211,11 @@ function getColumns(): ColumnDef<Account>[] {
|
||||
return (
|
||||
<div className={'flex justify-end'}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<Button variant={'outline'} size={'icon'}>
|
||||
<EllipsisVertical className={'h-4'} />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant={'outline'} size={'icon'}>
|
||||
<EllipsisVertical className={'h-4'} />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent align={'end'}>
|
||||
<DropdownMenuGroup>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useState, useTransition } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||
@@ -42,7 +41,7 @@ export function AdminBanUserDialog(
|
||||
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={setOpen}>
|
||||
<AlertDialogTrigger render={props.children as React.ReactElement} />
|
||||
<AlertDialogTrigger asChild>{props.children}</AlertDialogTrigger>
|
||||
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
@@ -61,9 +60,8 @@ export function AdminBanUserDialog(
|
||||
}
|
||||
|
||||
function BanUserForm(props: { userId: string; onSuccess: () => void }) {
|
||||
const { execute, isPending, hasErrored } = useAction(banUserAction, {
|
||||
onSuccess: () => props.onSuccess(),
|
||||
});
|
||||
const [pending, startTransition] = useTransition();
|
||||
const [error, setError] = useState<boolean>(false);
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(BanUserSchema),
|
||||
@@ -78,9 +76,18 @@ function BanUserForm(props: { userId: string; onSuccess: () => void }) {
|
||||
<form
|
||||
data-test={'admin-ban-user-form'}
|
||||
className={'flex flex-col space-y-8'}
|
||||
onSubmit={form.handleSubmit((data) => execute(data))}
|
||||
onSubmit={form.handleSubmit((data) => {
|
||||
startTransition(async () => {
|
||||
try {
|
||||
await banUserAction(data);
|
||||
props.onSuccess();
|
||||
} catch {
|
||||
setError(true);
|
||||
}
|
||||
});
|
||||
})}
|
||||
>
|
||||
<If condition={hasErrored}>
|
||||
<If condition={error}>
|
||||
<Alert variant={'destructive'}>
|
||||
<AlertTitle>Error</AlertTitle>
|
||||
|
||||
@@ -118,10 +125,10 @@ function BanUserForm(props: { userId: string; onSuccess: () => void }) {
|
||||
/>
|
||||
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isPending}>Cancel</AlertDialogCancel>
|
||||
<AlertDialogCancel disabled={pending}>Cancel</AlertDialogCancel>
|
||||
|
||||
<Button disabled={isPending} type={'submit'} variant={'destructive'}>
|
||||
{isPending ? 'Banning...' : 'Ban User'}
|
||||
<Button disabled={pending} type={'submit'} variant={'destructive'}>
|
||||
{pending ? 'Banning...' : 'Ban User'}
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</form>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useState, useTransition } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||
@@ -39,6 +38,8 @@ import {
|
||||
} from '../lib/server/schema/create-user.schema';
|
||||
|
||||
export function AdminCreateUserDialog(props: React.PropsWithChildren) {
|
||||
const [pending, startTransition] = useTransition();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const form = useForm({
|
||||
@@ -51,19 +52,28 @@ export function AdminCreateUserDialog(props: React.PropsWithChildren) {
|
||||
mode: 'onChange',
|
||||
});
|
||||
|
||||
const { execute, isPending, result } = useAction(createUserAction, {
|
||||
onSuccess: () => {
|
||||
toast.success('User created successfully');
|
||||
form.reset();
|
||||
setOpen(false);
|
||||
},
|
||||
});
|
||||
const onSubmit = (data: CreateUserSchemaType) => {
|
||||
startTransition(async () => {
|
||||
try {
|
||||
const result = await createUserAction(data);
|
||||
|
||||
const error = result.serverError;
|
||||
if (result.success) {
|
||||
toast.success('User creates successfully');
|
||||
form.reset();
|
||||
|
||||
setOpen(false);
|
||||
}
|
||||
|
||||
setError(null);
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : 'Error');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={setOpen}>
|
||||
<AlertDialogTrigger render={props.children as React.ReactElement} />
|
||||
<AlertDialogTrigger asChild>{props.children}</AlertDialogTrigger>
|
||||
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
@@ -78,9 +88,7 @@ export function AdminCreateUserDialog(props: React.PropsWithChildren) {
|
||||
<form
|
||||
data-test={'admin-create-user-form'}
|
||||
className={'flex flex-col space-y-4'}
|
||||
onSubmit={form.handleSubmit((data: CreateUserSchemaType) =>
|
||||
execute(data),
|
||||
)}
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
>
|
||||
<If condition={!!error}>
|
||||
<Alert variant={'destructive'}>
|
||||
@@ -158,8 +166,8 @@ export function AdminCreateUserDialog(props: React.PropsWithChildren) {
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
|
||||
<Button disabled={isPending} type={'submit'}>
|
||||
{isPending ? 'Creating...' : 'Create User'}
|
||||
<Button disabled={pending} type={'submit'}>
|
||||
{pending ? 'Creating...' : 'Create User'}
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</form>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useTransition } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||
@@ -36,7 +37,8 @@ export function AdminDeleteAccountDialog(
|
||||
accountId: string;
|
||||
}>,
|
||||
) {
|
||||
const { execute, isPending, hasErrored } = useAction(deleteAccountAction);
|
||||
const [pending, startTransition] = useTransition();
|
||||
const [error, setError] = useState<boolean>(false);
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(DeleteAccountSchema),
|
||||
@@ -48,7 +50,7 @@ export function AdminDeleteAccountDialog(
|
||||
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger render={props.children as React.ReactElement} />
|
||||
<AlertDialogTrigger asChild>{props.children}</AlertDialogTrigger>
|
||||
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
@@ -63,11 +65,20 @@ export function AdminDeleteAccountDialog(
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
data-test={'admin-delete-account-form'}
|
||||
data-form={'admin-delete-account-form'}
|
||||
className={'flex flex-col space-y-8'}
|
||||
onSubmit={form.handleSubmit((data) => execute(data))}
|
||||
onSubmit={form.handleSubmit((data) => {
|
||||
startTransition(async () => {
|
||||
try {
|
||||
await deleteAccountAction(data);
|
||||
setError(false);
|
||||
} catch {
|
||||
setError(true);
|
||||
}
|
||||
});
|
||||
})}
|
||||
>
|
||||
<If condition={hasErrored}>
|
||||
<If condition={error}>
|
||||
<Alert variant={'destructive'}>
|
||||
<AlertTitle>Error</AlertTitle>
|
||||
|
||||
@@ -109,11 +120,11 @@ export function AdminDeleteAccountDialog(
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
|
||||
<Button
|
||||
disabled={isPending}
|
||||
disabled={pending}
|
||||
type={'submit'}
|
||||
variant={'destructive'}
|
||||
>
|
||||
{isPending ? 'Deleting...' : 'Delete'}
|
||||
{pending ? 'Deleting...' : 'Delete'}
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</form>
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useTransition } from 'react';
|
||||
|
||||
import { isRedirectError } from 'next/dist/client/components/redirect-error';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||
@@ -36,7 +39,8 @@ export function AdminDeleteUserDialog(
|
||||
userId: string;
|
||||
}>,
|
||||
) {
|
||||
const { execute, isPending, hasErrored } = useAction(deleteUserAction);
|
||||
const [pending, startTransition] = useTransition();
|
||||
const [error, setError] = useState<boolean>(false);
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(DeleteUserSchema),
|
||||
@@ -48,7 +52,7 @@ export function AdminDeleteUserDialog(
|
||||
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger render={props.children as React.ReactElement} />
|
||||
<AlertDialogTrigger asChild>{props.children}</AlertDialogTrigger>
|
||||
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
@@ -65,9 +69,23 @@ export function AdminDeleteUserDialog(
|
||||
<form
|
||||
data-test={'admin-delete-user-form'}
|
||||
className={'flex flex-col space-y-8'}
|
||||
onSubmit={form.handleSubmit((data) => execute(data))}
|
||||
onSubmit={form.handleSubmit((data) => {
|
||||
startTransition(async () => {
|
||||
try {
|
||||
await deleteUserAction(data);
|
||||
|
||||
setError(false);
|
||||
} catch {
|
||||
if (isRedirectError(error)) {
|
||||
// Do nothing
|
||||
} else {
|
||||
setError(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
})}
|
||||
>
|
||||
<If condition={hasErrored}>
|
||||
<If condition={error}>
|
||||
<Alert variant={'destructive'}>
|
||||
<AlertTitle>Error</AlertTitle>
|
||||
|
||||
@@ -109,11 +127,11 @@ export function AdminDeleteUserDialog(
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
|
||||
<Button
|
||||
disabled={isPending}
|
||||
disabled={pending}
|
||||
type={'submit'}
|
||||
variant={'destructive'}
|
||||
>
|
||||
{isPending ? 'Deleting...' : 'Delete'}
|
||||
{pending ? 'Deleting...' : 'Delete'}
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</form>
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useState, useTransition } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { useSupabase } from '@kit/supabase/hooks/use-supabase';
|
||||
@@ -54,13 +53,8 @@ export function AdminImpersonateUserDialog(
|
||||
refreshToken: string;
|
||||
}>();
|
||||
|
||||
const { execute, isPending, hasErrored } = useAction(impersonateUserAction, {
|
||||
onSuccess: ({ data }) => {
|
||||
if (data) {
|
||||
setTokens(data);
|
||||
}
|
||||
},
|
||||
});
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [error, setError] = useState<boolean | null>(null);
|
||||
|
||||
if (tokens) {
|
||||
return (
|
||||
@@ -74,7 +68,7 @@ export function AdminImpersonateUserDialog(
|
||||
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger render={props.children as React.ReactElement} />
|
||||
<AlertDialogTrigger asChild>{props.children}</AlertDialogTrigger>
|
||||
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
@@ -97,9 +91,19 @@ export function AdminImpersonateUserDialog(
|
||||
<form
|
||||
data-test={'admin-impersonate-user-form'}
|
||||
className={'flex flex-col space-y-8'}
|
||||
onSubmit={form.handleSubmit((data) => execute(data))}
|
||||
onSubmit={form.handleSubmit((data) => {
|
||||
startTransition(async () => {
|
||||
try {
|
||||
const result = await impersonateUserAction(data);
|
||||
|
||||
setTokens(result);
|
||||
} catch {
|
||||
setError(true);
|
||||
}
|
||||
});
|
||||
})}
|
||||
>
|
||||
<If condition={hasErrored}>
|
||||
<If condition={error}>
|
||||
<Alert variant={'destructive'}>
|
||||
<AlertTitle>Error</AlertTitle>
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useState, useTransition } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||
@@ -42,7 +41,7 @@ export function AdminReactivateUserDialog(
|
||||
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={setOpen}>
|
||||
<AlertDialogTrigger render={props.children as React.ReactElement} />
|
||||
<AlertDialogTrigger asChild>{props.children}</AlertDialogTrigger>
|
||||
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
@@ -63,9 +62,8 @@ export function AdminReactivateUserDialog(
|
||||
}
|
||||
|
||||
function ReactivateUserForm(props: { userId: string; onSuccess: () => void }) {
|
||||
const { execute, isPending, hasErrored } = useAction(reactivateUserAction, {
|
||||
onSuccess: () => props.onSuccess(),
|
||||
});
|
||||
const [pending, startTransition] = useTransition();
|
||||
const [error, setError] = useState<boolean>(false);
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(ReactivateUserSchema),
|
||||
@@ -80,9 +78,18 @@ function ReactivateUserForm(props: { userId: string; onSuccess: () => void }) {
|
||||
<form
|
||||
data-test={'admin-reactivate-user-form'}
|
||||
className={'flex flex-col space-y-8'}
|
||||
onSubmit={form.handleSubmit((data) => execute(data))}
|
||||
onSubmit={form.handleSubmit((data) => {
|
||||
startTransition(async () => {
|
||||
try {
|
||||
await reactivateUserAction(data);
|
||||
props.onSuccess();
|
||||
} catch {
|
||||
setError(true);
|
||||
}
|
||||
});
|
||||
})}
|
||||
>
|
||||
<If condition={hasErrored}>
|
||||
<If condition={error}>
|
||||
<Alert variant={'destructive'}>
|
||||
<AlertTitle>Error</AlertTitle>
|
||||
|
||||
@@ -120,10 +127,10 @@ function ReactivateUserForm(props: { userId: string; onSuccess: () => void }) {
|
||||
/>
|
||||
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isPending}>Cancel</AlertDialogCancel>
|
||||
<AlertDialogCancel disabled={pending}>Cancel</AlertDialogCancel>
|
||||
|
||||
<Button disabled={isPending} type={'submit'}>
|
||||
{isPending ? 'Reactivating...' : 'Reactivate User'}
|
||||
<Button disabled={pending} type={'submit'}>
|
||||
{pending ? 'Reactivating...' : 'Reactivate User'}
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</form>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useTransition } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||
import {
|
||||
@@ -50,22 +51,33 @@ export function AdminResetPasswordDialog(
|
||||
},
|
||||
});
|
||||
|
||||
const { execute, isPending, hasErrored, hasSucceeded } = useAction(
|
||||
resetPasswordAction,
|
||||
{
|
||||
onSuccess: () => {
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [success, setSuccess] = useState(false);
|
||||
|
||||
const onSubmit = form.handleSubmit((data) => {
|
||||
setError(null);
|
||||
setSuccess(false);
|
||||
|
||||
startTransition(async () => {
|
||||
try {
|
||||
await resetPasswordAction(data);
|
||||
|
||||
setSuccess(true);
|
||||
form.reset({ userId: props.userId, confirmation: '' });
|
||||
|
||||
toast.success('Password reset email successfully sent');
|
||||
},
|
||||
onError: () => {
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : String(e));
|
||||
|
||||
toast.error('We hit an error. Please read the logs.');
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger render={props.children as React.ReactElement} />
|
||||
<AlertDialogTrigger asChild>{props.children}</AlertDialogTrigger>
|
||||
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
@@ -78,10 +90,7 @@ export function AdminResetPasswordDialog(
|
||||
|
||||
<div className="relative">
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit((data) => execute(data))}
|
||||
className="space-y-4"
|
||||
>
|
||||
<form onSubmit={onSubmit} className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="confirmation"
|
||||
@@ -106,7 +115,7 @@ export function AdminResetPasswordDialog(
|
||||
)}
|
||||
/>
|
||||
|
||||
<If condition={hasErrored}>
|
||||
<If condition={!!error}>
|
||||
<Alert variant="destructive">
|
||||
<AlertTitle>
|
||||
We encountered an error while sending the email
|
||||
@@ -118,7 +127,7 @@ export function AdminResetPasswordDialog(
|
||||
</Alert>
|
||||
</If>
|
||||
|
||||
<If condition={hasSucceeded}>
|
||||
<If condition={success}>
|
||||
<Alert>
|
||||
<AlertTitle>
|
||||
Password reset email sent successfully
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
import { enhanceAction } from '@kit/next/actions';
|
||||
import { getLogger } from '@kit/shared/logger';
|
||||
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
@@ -18,168 +19,212 @@ import { CreateUserSchema } from './schema/create-user.schema';
|
||||
import { ResetPasswordSchema } from './schema/reset-password.schema';
|
||||
import { createAdminAccountsService } from './services/admin-accounts.service';
|
||||
import { createAdminAuthUserService } from './services/admin-auth-user.service';
|
||||
import { adminActionClient } from './utils/admin-action-client';
|
||||
import { adminAction } from './utils/admin-action';
|
||||
|
||||
/**
|
||||
* @name banUserAction
|
||||
* @description Ban a user from the system.
|
||||
*/
|
||||
export const banUserAction = adminActionClient
|
||||
.schema(BanUserSchema)
|
||||
.action(async ({ parsedInput: { userId } }) => {
|
||||
const service = getAdminAuthService();
|
||||
const logger = await getLogger();
|
||||
export const banUserAction = adminAction(
|
||||
enhanceAction(
|
||||
async ({ userId }) => {
|
||||
const service = getAdminAuthService();
|
||||
const logger = await getLogger();
|
||||
|
||||
logger.info({ userId }, `Super Admin is banning user...`);
|
||||
logger.info({ userId }, `Super Admin is banning user...`);
|
||||
|
||||
const { error } = await service.banUser(userId);
|
||||
const { error } = await service.banUser(userId);
|
||||
|
||||
if (error) {
|
||||
logger.error({ error }, `Error banning user`);
|
||||
throw new Error('Error banning user');
|
||||
}
|
||||
if (error) {
|
||||
logger.error({ error }, `Error banning user`);
|
||||
|
||||
revalidateAdmin();
|
||||
return {
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
|
||||
logger.info({ userId }, `Super Admin has successfully banned user`);
|
||||
});
|
||||
revalidateAdmin();
|
||||
|
||||
logger.info({ userId }, `Super Admin has successfully banned user`);
|
||||
},
|
||||
{
|
||||
schema: BanUserSchema,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* @name reactivateUserAction
|
||||
* @description Reactivate a user in the system.
|
||||
*/
|
||||
export const reactivateUserAction = adminActionClient
|
||||
.schema(ReactivateUserSchema)
|
||||
.action(async ({ parsedInput: { userId } }) => {
|
||||
const service = getAdminAuthService();
|
||||
const logger = await getLogger();
|
||||
export const reactivateUserAction = adminAction(
|
||||
enhanceAction(
|
||||
async ({ userId }) => {
|
||||
const service = getAdminAuthService();
|
||||
const logger = await getLogger();
|
||||
|
||||
logger.info({ userId }, `Super Admin is reactivating user...`);
|
||||
logger.info({ userId }, `Super Admin is reactivating user...`);
|
||||
|
||||
const { error } = await service.reactivateUser(userId);
|
||||
const { error } = await service.reactivateUser(userId);
|
||||
|
||||
if (error) {
|
||||
logger.error({ error }, `Error reactivating user`);
|
||||
throw new Error('Error reactivating user');
|
||||
}
|
||||
if (error) {
|
||||
logger.error({ error }, `Error reactivating user`);
|
||||
|
||||
revalidateAdmin();
|
||||
return {
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
|
||||
logger.info({ userId }, `Super Admin has successfully reactivated user`);
|
||||
});
|
||||
revalidateAdmin();
|
||||
|
||||
logger.info({ userId }, `Super Admin has successfully reactivated user`);
|
||||
},
|
||||
{
|
||||
schema: ReactivateUserSchema,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* @name impersonateUserAction
|
||||
* @description Impersonate a user in the system.
|
||||
*/
|
||||
export const impersonateUserAction = adminActionClient
|
||||
.schema(ImpersonateUserSchema)
|
||||
.action(async ({ parsedInput: { userId } }) => {
|
||||
const service = getAdminAuthService();
|
||||
const logger = await getLogger();
|
||||
export const impersonateUserAction = adminAction(
|
||||
enhanceAction(
|
||||
async ({ userId }) => {
|
||||
const service = getAdminAuthService();
|
||||
const logger = await getLogger();
|
||||
|
||||
logger.info({ userId }, `Super Admin is impersonating user...`);
|
||||
logger.info({ userId }, `Super Admin is impersonating user...`);
|
||||
|
||||
return await service.impersonateUser(userId);
|
||||
});
|
||||
return await service.impersonateUser(userId);
|
||||
},
|
||||
{
|
||||
schema: ImpersonateUserSchema,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* @name deleteUserAction
|
||||
* @description Delete a user from the system.
|
||||
*/
|
||||
export const deleteUserAction = adminActionClient
|
||||
.schema(DeleteUserSchema)
|
||||
.action(async ({ parsedInput: { userId } }) => {
|
||||
const service = getAdminAuthService();
|
||||
const logger = await getLogger();
|
||||
export const deleteUserAction = adminAction(
|
||||
enhanceAction(
|
||||
async ({ userId }) => {
|
||||
const service = getAdminAuthService();
|
||||
const logger = await getLogger();
|
||||
|
||||
logger.info({ userId }, `Super Admin is deleting user...`);
|
||||
logger.info({ userId }, `Super Admin is deleting user...`);
|
||||
|
||||
await service.deleteUser(userId);
|
||||
await service.deleteUser(userId);
|
||||
|
||||
logger.info({ userId }, `Super Admin has successfully deleted user`);
|
||||
logger.info({ userId }, `Super Admin has successfully deleted user`);
|
||||
|
||||
redirect('/admin/accounts');
|
||||
});
|
||||
return redirect('/admin/accounts');
|
||||
},
|
||||
{
|
||||
schema: DeleteUserSchema,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* @name deleteAccountAction
|
||||
* @description Delete an account from the system.
|
||||
*/
|
||||
export const deleteAccountAction = adminActionClient
|
||||
.schema(DeleteAccountSchema)
|
||||
.action(async ({ parsedInput: { accountId } }) => {
|
||||
const service = getAdminAccountsService();
|
||||
const logger = await getLogger();
|
||||
export const deleteAccountAction = adminAction(
|
||||
enhanceAction(
|
||||
async ({ accountId }) => {
|
||||
const service = getAdminAccountsService();
|
||||
const logger = await getLogger();
|
||||
|
||||
logger.info({ accountId }, `Super Admin is deleting account...`);
|
||||
logger.info({ accountId }, `Super Admin is deleting account...`);
|
||||
|
||||
await service.deleteAccount(accountId);
|
||||
await service.deleteAccount(accountId);
|
||||
|
||||
revalidateAdmin();
|
||||
revalidateAdmin();
|
||||
|
||||
logger.info({ accountId }, `Super Admin has successfully deleted account`);
|
||||
logger.info(
|
||||
{ accountId },
|
||||
`Super Admin has successfully deleted account`,
|
||||
);
|
||||
|
||||
redirect('/admin/accounts');
|
||||
});
|
||||
return redirect('/admin/accounts');
|
||||
},
|
||||
{
|
||||
schema: DeleteAccountSchema,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* @name createUserAction
|
||||
* @description Create a new user in the system.
|
||||
*/
|
||||
export const createUserAction = adminActionClient
|
||||
.schema(CreateUserSchema)
|
||||
.action(async ({ parsedInput: { email, password, emailConfirm } }) => {
|
||||
const adminClient = getSupabaseServerAdminClient();
|
||||
const logger = await getLogger();
|
||||
export const createUserAction = adminAction(
|
||||
enhanceAction(
|
||||
async ({ email, password, emailConfirm }) => {
|
||||
const adminClient = getSupabaseServerAdminClient();
|
||||
const logger = await getLogger();
|
||||
|
||||
logger.info({ email }, `Super Admin is creating a new user...`);
|
||||
logger.info({ email }, `Super Admin is creating a new user...`);
|
||||
|
||||
const { data, error } = await adminClient.auth.admin.createUser({
|
||||
email,
|
||||
password,
|
||||
email_confirm: emailConfirm,
|
||||
});
|
||||
const { data, error } = await adminClient.auth.admin.createUser({
|
||||
email,
|
||||
password,
|
||||
email_confirm: emailConfirm,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
logger.error({ error }, `Error creating user`);
|
||||
throw new Error(`Error creating user: ${error.message}`);
|
||||
}
|
||||
if (error) {
|
||||
logger.error({ error }, `Error creating user`);
|
||||
throw new Error(`Error creating user: ${error.message}`);
|
||||
}
|
||||
|
||||
logger.info(
|
||||
{ userId: data.user.id },
|
||||
`Super Admin has successfully created a new user`,
|
||||
);
|
||||
logger.info(
|
||||
{ userId: data.user.id },
|
||||
`Super Admin has successfully created a new user`,
|
||||
);
|
||||
|
||||
revalidatePath(`/admin/accounts`);
|
||||
revalidatePath(`/admin/accounts`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
user: data.user,
|
||||
};
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
user: data.user,
|
||||
};
|
||||
},
|
||||
{
|
||||
schema: CreateUserSchema,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* @name resetPasswordAction
|
||||
* @description Reset a user's password by sending a password reset email.
|
||||
*/
|
||||
export const resetPasswordAction = adminActionClient
|
||||
.schema(ResetPasswordSchema)
|
||||
.action(async ({ parsedInput: { userId } }) => {
|
||||
const service = getAdminAuthService();
|
||||
const logger = await getLogger();
|
||||
export const resetPasswordAction = adminAction(
|
||||
enhanceAction(
|
||||
async ({ userId }) => {
|
||||
const service = getAdminAuthService();
|
||||
const logger = await getLogger();
|
||||
|
||||
logger.info({ userId }, `Super Admin is resetting user password...`);
|
||||
logger.info({ userId }, `Super Admin is resetting user password...`);
|
||||
|
||||
const result = await service.resetPassword(userId);
|
||||
const result = await service.resetPassword(userId);
|
||||
|
||||
logger.info(
|
||||
{ userId },
|
||||
`Super Admin has successfully sent password reset email`,
|
||||
);
|
||||
logger.info(
|
||||
{ userId },
|
||||
`Super Admin has successfully sent password reset email`,
|
||||
);
|
||||
|
||||
return result;
|
||||
});
|
||||
return result;
|
||||
},
|
||||
{
|
||||
schema: ResetPasswordSchema,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
function revalidateAdmin() {
|
||||
revalidatePath(`/admin/accounts/[id]`, 'page');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
const ConfirmationSchema = z.object({
|
||||
confirmation: z.custom<string>((value) => value === 'CONFIRM'),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const CreateUserSchema = z.object({
|
||||
email: z.string().email({ message: 'Please enter a valid email address' }),
|
||||
@@ -8,4 +8,4 @@ export const CreateUserSchema = z.object({
|
||||
emailConfirm: z.boolean().default(false).optional(),
|
||||
});
|
||||
|
||||
export type CreateUserSchemaType = z.output<typeof CreateUserSchema>;
|
||||
export type CreateUserSchemaType = z.infer<typeof CreateUserSchema>;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
/**
|
||||
* Schema for resetting a user's password
|
||||
|
||||
@@ -2,7 +2,7 @@ import 'server-only';
|
||||
|
||||
import { SupabaseClient } from '@supabase/supabase-js';
|
||||
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { Database } from '@kit/supabase/database';
|
||||
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import 'server-only';
|
||||
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
import { authActionClient } from '@kit/next/safe-action';
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
|
||||
import { isSuperAdmin } from './is-super-admin';
|
||||
|
||||
/**
|
||||
* @name adminActionClient
|
||||
* @description Safe action client for admin-only actions.
|
||||
* Extends authActionClient with super admin verification.
|
||||
*/
|
||||
export const adminActionClient = authActionClient.use(async ({ next, ctx }) => {
|
||||
const isAdmin = await isSuperAdmin(getSupabaseServerClient());
|
||||
|
||||
if (!isAdmin) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return next({ ctx });
|
||||
});
|
||||
@@ -28,14 +28,15 @@
|
||||
"@kit/tsconfig": "workspace:*",
|
||||
"@kit/ui": "workspace:*",
|
||||
"@marsidev/react-turnstile": "catalog:",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@supabase/supabase-js": "catalog:",
|
||||
"@tanstack/react-query": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"@types/react": "catalog:",
|
||||
"lucide-react": "catalog:",
|
||||
"next": "catalog:",
|
||||
"next-intl": "catalog:",
|
||||
"react-hook-form": "catalog:",
|
||||
"react-i18next": "catalog:",
|
||||
"sonner": "^2.0.7",
|
||||
"zod": "catalog:"
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { TriangleAlert } from 'lucide-react';
|
||||
import { ExclamationTriangleIcon } from '@radix-ui/react-icons';
|
||||
|
||||
import {
|
||||
WeakPasswordError,
|
||||
@@ -33,25 +33,23 @@ export function AuthErrorAlert({
|
||||
return <WeakPasswordErrorAlert reasons={error.reasons} />;
|
||||
}
|
||||
|
||||
const DefaultError = <Trans i18nKey="auth.errors.default" />;
|
||||
|
||||
const errorCode =
|
||||
error instanceof Error
|
||||
? 'code' in error && typeof error.code === 'string'
|
||||
? error.code
|
||||
: error.message
|
||||
: error;
|
||||
const DefaultError = <Trans i18nKey="auth:errors.default" />;
|
||||
const errorCode = error instanceof Error ? error.message : error;
|
||||
|
||||
return (
|
||||
<Alert variant={'destructive'}>
|
||||
<TriangleAlert className={'w-4'} />
|
||||
<ExclamationTriangleIcon className={'w-4'} />
|
||||
|
||||
<AlertTitle>
|
||||
<Trans i18nKey={`auth.errorAlertHeading`} />
|
||||
<Trans i18nKey={`auth:errorAlertHeading`} />
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription data-test={'auth-error-message'}>
|
||||
<Trans i18nKey={`auth.errors.${errorCode}`} defaults={DefaultError} />
|
||||
<Trans
|
||||
i18nKey={`auth:errors.${errorCode}`}
|
||||
defaults={'<DefaultError />'}
|
||||
components={{ DefaultError }}
|
||||
/>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
@@ -64,21 +62,21 @@ function WeakPasswordErrorAlert({
|
||||
}) {
|
||||
return (
|
||||
<Alert variant={'destructive'}>
|
||||
<TriangleAlert className={'w-4'} />
|
||||
<ExclamationTriangleIcon className={'w-4'} />
|
||||
|
||||
<AlertTitle>
|
||||
<Trans i18nKey={'auth.errors.weakPassword.title'} />
|
||||
<Trans i18nKey={'auth:errors.weakPassword.title'} />
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription data-test={'auth-error-message'}>
|
||||
<Trans i18nKey={'auth.errors.weakPassword.description'} />
|
||||
<Trans i18nKey={'auth:errors.weakPassword.description'} />
|
||||
|
||||
{reasons.length > 0 && (
|
||||
<ul className="mt-2 list-inside list-disc space-y-1 text-xs">
|
||||
{reasons.map((reason) => (
|
||||
<li key={reason}>
|
||||
<Trans
|
||||
i18nKey={`auth.errors.weakPassword.reasons.${reason}`}
|
||||
i18nKey={`auth:errors.weakPassword.reasons.${reason}`}
|
||||
defaults={reason}
|
||||
/>
|
||||
</li>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { Mail } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import {
|
||||
InputGroup,
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
} from '@kit/ui/input-group';
|
||||
|
||||
export function EmailInput(props: React.ComponentProps<'input'>) {
|
||||
const t = useTranslations('auth');
|
||||
const { t } = useTranslation('auth');
|
||||
|
||||
return (
|
||||
<InputGroup className="dark:bg-background">
|
||||
|
||||
@@ -7,7 +7,7 @@ import Link from 'next/link';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
|
||||
import { UserCheck } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Alert, AlertDescription } from '@kit/ui/alert';
|
||||
import { If } from '@kit/ui/if';
|
||||
@@ -36,7 +36,7 @@ export function ExistingAccountHintImpl({
|
||||
useLastAuthMethod();
|
||||
|
||||
const params = useSearchParams();
|
||||
const t = useTranslations();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const isInvite = params.get('invite_token');
|
||||
|
||||
@@ -53,13 +53,13 @@ export function ExistingAccountHintImpl({
|
||||
|
||||
switch (methodType) {
|
||||
case 'password':
|
||||
return 'auth.methodPassword';
|
||||
return 'auth:methodPassword';
|
||||
case 'otp':
|
||||
return 'auth.methodOtp';
|
||||
return 'auth:methodOtp';
|
||||
case 'magic_link':
|
||||
return 'auth.methodMagicLink';
|
||||
return 'auth:methodMagicLink';
|
||||
default:
|
||||
return 'auth.methodDefault';
|
||||
return 'auth:methodDefault';
|
||||
}
|
||||
}, [methodType, isOAuth, providerName]);
|
||||
|
||||
@@ -73,10 +73,10 @@ export function ExistingAccountHintImpl({
|
||||
<Alert data-test={'existing-account-hint'} className={className}>
|
||||
<UserCheck className="h-4 w-4" />
|
||||
|
||||
<AlertDescription className={'text-xs'}>
|
||||
<AlertDescription>
|
||||
<Trans
|
||||
i18nKey="auth.existingAccountHint"
|
||||
values={{ methodName: t(methodDescription) }}
|
||||
i18nKey="auth:existingAccountHint"
|
||||
values={{ method: t(methodDescription) }}
|
||||
components={{
|
||||
method: <span className="font-medium" />,
|
||||
signInLink: (
|
||||
|
||||
@@ -32,13 +32,13 @@ function LastAuthMethodHintImpl({ className }: LastAuthMethodHintProps) {
|
||||
const methodKey = useMemo(() => {
|
||||
switch (methodType) {
|
||||
case 'password':
|
||||
return 'auth.methodPassword';
|
||||
return 'auth:methodPassword';
|
||||
case 'otp':
|
||||
return 'auth.methodOtp';
|
||||
return 'auth:methodOtp';
|
||||
case 'magic_link':
|
||||
return 'auth.methodMagicLink';
|
||||
return 'auth:methodMagicLink';
|
||||
case 'oauth':
|
||||
return 'auth.methodOauth';
|
||||
return 'auth:methodOauth';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -61,10 +61,10 @@ function LastAuthMethodHintImpl({ className }: LastAuthMethodHintProps) {
|
||||
<Lightbulb className="h-3 w-3" />
|
||||
|
||||
<span>
|
||||
<Trans i18nKey="auth.lastUsedMethodPrefix" />{' '}
|
||||
<Trans i18nKey="auth:lastUsedMethodPrefix" />{' '}
|
||||
<If condition={isOAuth && Boolean(providerName)}>
|
||||
<Trans
|
||||
i18nKey="auth.methodOauthWithProvider"
|
||||
i18nKey="auth:methodOauthWithProvider"
|
||||
values={{ provider: providerName }}
|
||||
components={{
|
||||
provider: <span className="text-muted-foreground font-medium" />,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Check, TriangleAlert } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { CheckIcon, ExclamationTriangleIcon } from '@radix-ui/react-icons';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import * as z from 'zod';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { useAppEvents } from '@kit/shared/events';
|
||||
import { useSignInWithOtp } from '@kit/supabase/hooks/use-sign-in-with-otp';
|
||||
@@ -44,7 +44,7 @@ export function MagicLinkAuthContainer({
|
||||
};
|
||||
}) {
|
||||
const captcha = useCaptcha({ siteKey: captchaSiteKey });
|
||||
const t = useTranslations();
|
||||
const { t } = useTranslation();
|
||||
const signInWithOtpMutation = useSignInWithOtp();
|
||||
const appEvents = useAppEvents();
|
||||
const { recordAuthMethod } = useLastAuthMethod();
|
||||
@@ -90,9 +90,9 @@ export function MagicLinkAuthContainer({
|
||||
};
|
||||
|
||||
toast.promise(promise, {
|
||||
loading: t('auth.sendingEmailLink'),
|
||||
success: t(`auth.sendLinkSuccessToast`),
|
||||
error: t(`auth.errors.linkTitle`),
|
||||
loading: t('auth:sendingEmailLink'),
|
||||
success: t(`auth:sendLinkSuccessToast`),
|
||||
error: t(`auth:errors.linkTitle`),
|
||||
});
|
||||
|
||||
captcha.reset();
|
||||
@@ -116,7 +116,7 @@ export function MagicLinkAuthContainer({
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans i18nKey={'common.emailAddress'} />
|
||||
<Trans i18nKey={'common:emailAddress'} />
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
@@ -133,20 +133,17 @@ export function MagicLinkAuthContainer({
|
||||
<TermsAndConditionsFormField />
|
||||
</If>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={signInWithOtpMutation.isPending || captchaLoading}
|
||||
>
|
||||
<Button disabled={signInWithOtpMutation.isPending || captchaLoading}>
|
||||
<If condition={captchaLoading}>
|
||||
<Trans i18nKey={'auth.verifyingCaptcha'} />
|
||||
<Trans i18nKey={'auth:verifyingCaptcha'} />
|
||||
</If>
|
||||
|
||||
<If condition={signInWithOtpMutation.isPending && !captchaLoading}>
|
||||
<Trans i18nKey={'auth.sendingEmailLink'} />
|
||||
<Trans i18nKey={'auth:sendingEmailLink'} />
|
||||
</If>
|
||||
|
||||
<If condition={!signInWithOtpMutation.isPending && !captchaLoading}>
|
||||
<Trans i18nKey={'auth.sendEmailLink'} />
|
||||
<Trans i18nKey={'auth:sendEmailLink'} />
|
||||
</If>
|
||||
</Button>
|
||||
</div>
|
||||
@@ -158,14 +155,14 @@ export function MagicLinkAuthContainer({
|
||||
function SuccessAlert() {
|
||||
return (
|
||||
<Alert variant={'success'}>
|
||||
<Check className={'h-4'} />
|
||||
<CheckIcon className={'h-4'} />
|
||||
|
||||
<AlertTitle>
|
||||
<Trans i18nKey={'auth.sendLinkSuccess'} />
|
||||
<Trans i18nKey={'auth:sendLinkSuccess'} />
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
<Trans i18nKey={'auth.sendLinkSuccessDescription'} />
|
||||
<Trans i18nKey={'auth:sendLinkSuccessDescription'} />
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
@@ -174,14 +171,14 @@ function SuccessAlert() {
|
||||
function ErrorAlert() {
|
||||
return (
|
||||
<Alert variant={'destructive'}>
|
||||
<TriangleAlert className={'h-4'} />
|
||||
<ExclamationTriangleIcon className={'h-4'} />
|
||||
|
||||
<AlertTitle>
|
||||
<Trans i18nKey={'auth.errors.linkTitle'} />
|
||||
<Trans i18nKey={'auth:errors.linkTitle'} />
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
<Trans i18nKey={'auth.errors.linkDescription'} />
|
||||
<Trans i18nKey={'auth:errors.linkDescription'} />
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
|
||||
@@ -5,10 +5,10 @@ import { useEffect, useEffectEvent } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { ExclamationTriangleIcon } from '@radix-ui/react-icons';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { TriangleAlert } from 'lucide-react';
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { useFetchAuthFactors } from '@kit/supabase/hooks/use-fetch-mfa-factors';
|
||||
import { useSignOut } from '@kit/supabase/hooks/use-sign-out';
|
||||
@@ -94,7 +94,7 @@ export function MultiFactorChallengeContainer({
|
||||
<div className={'flex flex-col items-center gap-y-6'}>
|
||||
<div className="flex flex-col items-center gap-y-4">
|
||||
<Heading level={5}>
|
||||
<Trans i18nKey={'auth.verifyCodeHeading'} />
|
||||
<Trans i18nKey={'auth:verifyCodeHeading'} />
|
||||
</Heading>
|
||||
</div>
|
||||
|
||||
@@ -102,15 +102,15 @@ export function MultiFactorChallengeContainer({
|
||||
<div className={'flex flex-col gap-y-4'}>
|
||||
<If condition={verifyMFAChallenge.error}>
|
||||
<Alert variant={'destructive'}>
|
||||
<TriangleAlert className={'h-5'} />
|
||||
<ExclamationTriangleIcon className={'h-5'} />
|
||||
|
||||
<AlertTitle>
|
||||
<Trans i18nKey={'account.invalidVerificationCodeHeading'} />
|
||||
<Trans i18nKey={'account:invalidVerificationCodeHeading'} />
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
<Trans
|
||||
i18nKey={'account.invalidVerificationCodeDescription'}
|
||||
i18nKey={'account:invalidVerificationCodeDescription'}
|
||||
/>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
@@ -143,7 +143,7 @@ export function MultiFactorChallengeContainer({
|
||||
|
||||
<FormDescription className="text-center">
|
||||
<Trans
|
||||
i18nKey={'account.verifyActivationCodeDescription'}
|
||||
i18nKey={'account:verifyActivationCodeDescription'}
|
||||
/>
|
||||
</FormDescription>
|
||||
|
||||
@@ -156,7 +156,6 @@ export function MultiFactorChallengeContainer({
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full"
|
||||
data-test={'submit-mfa-button'}
|
||||
disabled={
|
||||
@@ -167,13 +166,13 @@ export function MultiFactorChallengeContainer({
|
||||
>
|
||||
<If condition={verifyMFAChallenge.isPending}>
|
||||
<span className={'animate-in fade-in slide-in-from-bottom-24'}>
|
||||
<Trans i18nKey={'account.verifyingCode'} />
|
||||
<Trans i18nKey={'account:verifyingCode'} />
|
||||
</span>
|
||||
</If>
|
||||
|
||||
<If condition={verifyMFAChallenge.isSuccess}>
|
||||
<span className={'animate-in fade-in slide-in-from-bottom-24'}>
|
||||
<Trans i18nKey={'auth.redirecting'} />
|
||||
<Trans i18nKey={'auth:redirecting'} />
|
||||
</span>
|
||||
</If>
|
||||
|
||||
@@ -182,7 +181,7 @@ export function MultiFactorChallengeContainer({
|
||||
!verifyMFAChallenge.isPending && !verifyMFAChallenge.isSuccess
|
||||
}
|
||||
>
|
||||
<Trans i18nKey={'account.submitVerificationCode'} />
|
||||
<Trans i18nKey={'account:submitVerificationCode'} />
|
||||
</If>
|
||||
</Button>
|
||||
</div>
|
||||
@@ -256,7 +255,7 @@ function FactorsListContainer({
|
||||
<Spinner />
|
||||
|
||||
<div className={'text-sm'}>
|
||||
<Trans i18nKey={'account.loadingFactors'} />
|
||||
<Trans i18nKey={'account:loadingFactors'} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -266,14 +265,14 @@ function FactorsListContainer({
|
||||
return (
|
||||
<div className={'w-full'}>
|
||||
<Alert variant={'destructive'}>
|
||||
<TriangleAlert className={'h-4'} />
|
||||
<ExclamationTriangleIcon className={'h-4'} />
|
||||
|
||||
<AlertTitle>
|
||||
<Trans i18nKey={'account.factorsListError'} />
|
||||
<Trans i18nKey={'account:factorsListError'} />
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
<Trans i18nKey={'account.factorsListErrorDescription'} />
|
||||
<Trans i18nKey={'account:factorsListErrorDescription'} />
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
@@ -286,7 +285,7 @@ function FactorsListContainer({
|
||||
<div className={'animate-in fade-in flex flex-col space-y-4 duration-500'}>
|
||||
<div>
|
||||
<span className={'font-medium'}>
|
||||
<Trans i18nKey={'account.selectFactor'} />
|
||||
<Trans i18nKey={'account:selectFactor'} />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -114,7 +114,7 @@ export const OauthProviders: React.FC<{
|
||||
}}
|
||||
>
|
||||
<Trans
|
||||
i18nKey={'auth.signInWithProvider'}
|
||||
i18nKey={'auth:signInWithProvider'}
|
||||
values={{
|
||||
provider: getProviderName(provider),
|
||||
}}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useRouter, useSearchParams } from 'next/navigation';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { useSignInWithOtp } from '@kit/supabase/hooks/use-sign-in-with-otp';
|
||||
import { useVerifyOtp } from '@kit/supabase/hooks/use-verify-otp';
|
||||
@@ -132,7 +132,7 @@ export function OtpSignInContainer(props: OtpSignInContainerProps) {
|
||||
</FormControl>
|
||||
|
||||
<FormDescription>
|
||||
<Trans i18nKey="common.otp.enterCodeFromEmail" />
|
||||
<Trans i18nKey="common:otp.enterCodeFromEmail" />
|
||||
</FormDescription>
|
||||
|
||||
<FormMessage />
|
||||
@@ -149,10 +149,10 @@ export function OtpSignInContainer(props: OtpSignInContainerProps) {
|
||||
{verifyMutation.isPending ? (
|
||||
<>
|
||||
<Spinner className="mr-2 h-4 w-4" />
|
||||
<Trans i18nKey="common.otp.verifying" />
|
||||
<Trans i18nKey="common:otp.verifying" />
|
||||
</>
|
||||
) : (
|
||||
<Trans i18nKey="common.otp.verifyCode" />
|
||||
<Trans i18nKey="common:otp.verifyCode" />
|
||||
)}
|
||||
</Button>
|
||||
|
||||
@@ -166,7 +166,7 @@ export function OtpSignInContainer(props: OtpSignInContainerProps) {
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Trans i18nKey="common.otp.requestNewCode" />
|
||||
<Trans i18nKey="common:otp.requestNewCode" />
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -191,7 +191,7 @@ function OtpEmailForm({
|
||||
defaultValues: { email: '' },
|
||||
});
|
||||
|
||||
const handleSendOtp = async ({ email }: z.output<typeof EmailSchema>) => {
|
||||
const handleSendOtp = async ({ email }: z.infer<typeof EmailSchema>) => {
|
||||
await signInMutation.mutateAsync({
|
||||
email,
|
||||
options: { captchaToken: captcha.token, shouldCreateUser },
|
||||
@@ -230,10 +230,10 @@ function OtpEmailForm({
|
||||
{signInMutation.isPending ? (
|
||||
<>
|
||||
<Spinner className="mr-2 h-4 w-4" />
|
||||
<Trans i18nKey="common.otp.sendingCode" />
|
||||
<Trans i18nKey="common:otp.sendingCode" />
|
||||
</>
|
||||
) : (
|
||||
<Trans i18nKey="common.otp.sendVerificationCode" />
|
||||
<Trans i18nKey="common:otp.sendVerificationCode" />
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import * as z from 'zod';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { useRequestResetPassword } from '@kit/supabase/hooks/use-request-reset-password';
|
||||
import { Alert, AlertDescription } from '@kit/ui/alert';
|
||||
@@ -31,7 +31,7 @@ export function PasswordResetRequestContainer(params: {
|
||||
redirectPath: string;
|
||||
captchaSiteKey?: string;
|
||||
}) {
|
||||
const t = useTranslations('auth');
|
||||
const { t } = useTranslation('auth');
|
||||
const resetPasswordMutation = useRequestResetPassword();
|
||||
const captcha = useCaptcha({ siteKey: params.captchaSiteKey });
|
||||
const captchaLoading = !captcha.isReady;
|
||||
@@ -51,7 +51,7 @@ export function PasswordResetRequestContainer(params: {
|
||||
<If condition={success}>
|
||||
<Alert variant={'success'}>
|
||||
<AlertDescription>
|
||||
<Trans i18nKey={'auth.passwordResetSuccessMessage'} />
|
||||
<Trans i18nKey={'auth:passwordResetSuccessMessage'} />
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</If>
|
||||
@@ -85,7 +85,7 @@ export function PasswordResetRequestContainer(params: {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans i18nKey={'common.emailAddress'} />
|
||||
<Trans i18nKey={'common:emailAddress'} />
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
@@ -111,15 +111,15 @@ export function PasswordResetRequestContainer(params: {
|
||||
!resetPasswordMutation.isPending && !captchaLoading
|
||||
}
|
||||
>
|
||||
<Trans i18nKey={'auth.passwordResetLabel'} />
|
||||
<Trans i18nKey={'auth:passwordResetLabel'} />
|
||||
</If>
|
||||
|
||||
<If condition={resetPasswordMutation.isPending}>
|
||||
<Trans i18nKey={'auth.passwordResetLabel'} />
|
||||
<Trans i18nKey={'auth:passwordResetLabel'} />
|
||||
</If>
|
||||
|
||||
<If condition={captchaLoading}>
|
||||
<Trans i18nKey={'auth.verifyingCaptcha'} />
|
||||
<Trans i18nKey={'auth:verifyingCaptcha'} />
|
||||
</If>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -27,7 +27,7 @@ export function PasswordSignInContainer({
|
||||
const captchaLoading = !captcha.isReady;
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (credentials: z.output<typeof PasswordSignInSchema>) => {
|
||||
async (credentials: z.infer<typeof PasswordSignInSchema>) => {
|
||||
try {
|
||||
const data = await signInMutation.mutateAsync({
|
||||
...credentials,
|
||||
|
||||
@@ -4,8 +4,8 @@ import Link from 'next/link';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { ArrowRight, Mail } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { z } from 'zod';
|
||||
|
||||
import { Button } from '@kit/ui/button';
|
||||
@@ -33,12 +33,12 @@ export function PasswordSignInForm({
|
||||
loading = false,
|
||||
redirecting = false,
|
||||
}: {
|
||||
onSubmit: (params: z.output<typeof PasswordSignInSchema>) => unknown;
|
||||
onSubmit: (params: z.infer<typeof PasswordSignInSchema>) => unknown;
|
||||
captchaLoading: boolean;
|
||||
loading: boolean;
|
||||
redirecting: boolean;
|
||||
}) {
|
||||
const t = useTranslations('auth');
|
||||
const { t } = useTranslation('auth');
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(PasswordSignInSchema),
|
||||
@@ -94,14 +94,15 @@ export function PasswordSignInForm({
|
||||
|
||||
<div>
|
||||
<Button
|
||||
nativeButton={false}
|
||||
render={<Link href={'/auth/password-reset'} />}
|
||||
asChild
|
||||
type={'button'}
|
||||
size={'sm'}
|
||||
variant={'link'}
|
||||
className={'text-xs'}
|
||||
>
|
||||
<Trans i18nKey={'auth.passwordForgottenQuestion'} />
|
||||
<Link href={'/auth/password-reset'}>
|
||||
<Trans i18nKey={'auth:passwordForgottenQuestion'} />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</FormItem>
|
||||
@@ -117,19 +118,19 @@ export function PasswordSignInForm({
|
||||
>
|
||||
<If condition={redirecting}>
|
||||
<span className={'animate-in fade-in slide-in-from-bottom-24'}>
|
||||
<Trans i18nKey={'auth.redirecting'} />
|
||||
<Trans i18nKey={'auth:redirecting'} />
|
||||
</span>
|
||||
</If>
|
||||
|
||||
<If condition={loading}>
|
||||
<span className={'animate-in fade-in slide-in-from-bottom-24'}>
|
||||
<Trans i18nKey={'auth.signingIn'} />
|
||||
<Trans i18nKey={'auth:signingIn'} />
|
||||
</span>
|
||||
</If>
|
||||
|
||||
<If condition={captchaLoading}>
|
||||
<span className={'animate-in fade-in slide-in-from-bottom-24'}>
|
||||
<Trans i18nKey={'auth.verifyingCaptcha'} />
|
||||
<Trans i18nKey={'auth:verifyingCaptcha'} />
|
||||
</span>
|
||||
</If>
|
||||
|
||||
@@ -139,7 +140,7 @@ export function PasswordSignInForm({
|
||||
'animate-in fade-in slide-in-from-bottom-24 flex items-center'
|
||||
}
|
||||
>
|
||||
<Trans i18nKey={'auth.signInWithEmail'} />
|
||||
<Trans i18nKey={'auth:signInWithEmail'} />
|
||||
|
||||
<ArrowRight
|
||||
className={
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { CheckCircle } from 'lucide-react';
|
||||
import { CheckCircledIcon } from '@radix-ui/react-icons';
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||
import { If } from '@kit/ui/if';
|
||||
@@ -71,14 +71,14 @@ export function EmailPasswordSignUpContainer({
|
||||
function SuccessAlert() {
|
||||
return (
|
||||
<Alert variant={'success'}>
|
||||
<CheckCircle className={'w-4'} />
|
||||
<CheckCircledIcon className={'w-4'} />
|
||||
|
||||
<AlertTitle>
|
||||
<Trans i18nKey={'auth.emailConfirmationAlertHeading'} />
|
||||
<Trans i18nKey={'auth:emailConfirmationAlertHeading'} />
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription data-test={'email-confirmation-alert'}>
|
||||
<Trans i18nKey={'auth.emailConfirmationAlertBody'} />
|
||||
<Trans i18nKey={'auth:emailConfirmationAlertBody'} />
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
|
||||
@@ -102,7 +102,7 @@ export function PasswordSignUpForm({
|
||||
</FormControl>
|
||||
|
||||
<FormDescription>
|
||||
<Trans i18nKey={'auth.repeatPasswordDescription'} />
|
||||
<Trans i18nKey={'auth:repeatPasswordDescription'} />
|
||||
</FormDescription>
|
||||
|
||||
<FormMessage />
|
||||
@@ -123,13 +123,13 @@ export function PasswordSignUpForm({
|
||||
>
|
||||
<If condition={captchaLoading}>
|
||||
<span className={'animate-in fade-in slide-in-from-bottom-24'}>
|
||||
<Trans i18nKey={'auth.verifyingCaptcha'} />
|
||||
<Trans i18nKey={'auth:verifyingCaptcha'} />
|
||||
</span>
|
||||
</If>
|
||||
|
||||
<If condition={loading && !captchaLoading}>
|
||||
<span className={'animate-in fade-in slide-in-from-bottom-24'}>
|
||||
<Trans i18nKey={'auth.signingUp'} />
|
||||
<Trans i18nKey={'auth:signingUp'} />
|
||||
</span>
|
||||
</If>
|
||||
|
||||
@@ -139,7 +139,7 @@ export function PasswordSignUpForm({
|
||||
'animate-in fade-in slide-in-from-bottom-24 flex items-center'
|
||||
}
|
||||
>
|
||||
<Trans i18nKey={'auth.signUpWithEmail'} />
|
||||
<Trans i18nKey={'auth:signUpWithEmail'} />
|
||||
|
||||
<ArrowRight
|
||||
className={
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { useSupabase } from '@kit/supabase/hooks/use-supabase';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||
@@ -40,12 +40,12 @@ export function ResendAuthLinkForm(props: {
|
||||
return (
|
||||
<Alert variant={'success'}>
|
||||
<AlertTitle>
|
||||
<Trans i18nKey={'auth.resendLinkSuccess'} />
|
||||
<Trans i18nKey={'auth:resendLinkSuccess'} />
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
<Trans
|
||||
i18nKey={'auth.resendLinkSuccessDescription'}
|
||||
i18nKey={'auth:resendLinkSuccessDescription'}
|
||||
defaults={'Success!'}
|
||||
/>
|
||||
</AlertDescription>
|
||||
@@ -85,17 +85,17 @@ export function ResendAuthLinkForm(props: {
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button type="submit" disabled={resendLink.isPending || captchaLoading}>
|
||||
<Button disabled={resendLink.isPending || captchaLoading}>
|
||||
<If condition={captchaLoading}>
|
||||
<Trans i18nKey={'auth.verifyingCaptcha'} />
|
||||
<Trans i18nKey={'auth:verifyingCaptcha'} />
|
||||
</If>
|
||||
|
||||
<If condition={resendLink.isPending && !captchaLoading}>
|
||||
<Trans i18nKey={'auth.resendingLink'} />
|
||||
<Trans i18nKey={'auth:resendingLink'} />
|
||||
</If>
|
||||
|
||||
<If condition={!resendLink.isPending && !captchaLoading}>
|
||||
<Trans i18nKey={'auth.resendLink'} defaults={'Resend Link'} />
|
||||
<Trans i18nKey={'auth:resendLink'} defaults={'Resend Link'} />
|
||||
</If>
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
@@ -86,7 +86,7 @@ export function SignInMethodsContainer(props: {
|
||||
|
||||
<div className="relative flex justify-center text-xs uppercase">
|
||||
<span className="bg-background text-muted-foreground px-2">
|
||||
<Trans i18nKey="auth.orContinueWith" />
|
||||
<Trans i18nKey="auth:orContinueWith" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -78,7 +78,7 @@ export function SignUpMethodsContainer(props: {
|
||||
|
||||
<div className="relative flex justify-center text-xs uppercase">
|
||||
<span className="bg-background text-muted-foreground px-2">
|
||||
<Trans i18nKey="auth.orContinueWith" />
|
||||
<Trans i18nKey="auth:orContinueWith" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -21,7 +21,7 @@ export function TermsAndConditionsFormField(
|
||||
|
||||
<div className={'text-xs'}>
|
||||
<Trans
|
||||
i18nKey={'auth.acceptTermsAndConditions'}
|
||||
i18nKey={'auth:acceptTermsAndConditions'}
|
||||
components={{
|
||||
TermsOfServiceLink: (
|
||||
<Link
|
||||
@@ -29,7 +29,7 @@ export function TermsAndConditionsFormField(
|
||||
className={'underline'}
|
||||
href={'/terms-of-service'}
|
||||
>
|
||||
<Trans i18nKey={'auth.termsOfService'} />
|
||||
<Trans i18nKey={'auth:termsOfService'} />
|
||||
</Link>
|
||||
),
|
||||
PrivacyPolicyLink: (
|
||||
@@ -38,7 +38,7 @@ export function TermsAndConditionsFormField(
|
||||
className={'underline'}
|
||||
href={'/privacy-policy'}
|
||||
>
|
||||
<Trans i18nKey={'auth.privacyPolicy'} />
|
||||
<Trans i18nKey={'auth:privacyPolicy'} />
|
||||
</Link>
|
||||
),
|
||||
}}
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { TriangleAlert } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { ExclamationTriangleIcon } from '@radix-ui/react-icons';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { useUpdateUser } from '@kit/supabase/hooks/use-update-user-mutation';
|
||||
@@ -31,7 +31,7 @@ export function UpdatePasswordForm(params: {
|
||||
}) {
|
||||
const updateUser = useUpdateUser();
|
||||
const router = useRouter();
|
||||
const t = useTranslations();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(PasswordResetSchema),
|
||||
@@ -68,7 +68,7 @@ export function UpdatePasswordForm(params: {
|
||||
|
||||
router.replace(params.redirectTo);
|
||||
|
||||
toast.success(t('account.updatePasswordSuccessMessage'));
|
||||
toast.success(t('account:updatePasswordSuccessMessage'));
|
||||
})}
|
||||
>
|
||||
<div className={'flex-col space-y-2.5'}>
|
||||
@@ -94,7 +94,7 @@ export function UpdatePasswordForm(params: {
|
||||
</FormControl>
|
||||
|
||||
<FormDescription>
|
||||
<Trans i18nKey={'common.repeatPassword'} />
|
||||
<Trans i18nKey={'common:repeatPassword'} />
|
||||
</FormDescription>
|
||||
|
||||
<FormMessage />
|
||||
@@ -107,7 +107,7 @@ export function UpdatePasswordForm(params: {
|
||||
type="submit"
|
||||
className={'w-full'}
|
||||
>
|
||||
<Trans i18nKey={'auth.passwordResetLabel'} />
|
||||
<Trans i18nKey={'auth:passwordResetLabel'} />
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -122,7 +122,7 @@ function ErrorState(props: {
|
||||
code: string;
|
||||
};
|
||||
}) {
|
||||
const t = useTranslations('auth');
|
||||
const { t } = useTranslation('auth');
|
||||
|
||||
const errorMessage = t(`errors.${props.error.code}`, {
|
||||
defaultValue: t('errors.resetPasswordError'),
|
||||
@@ -131,17 +131,17 @@ function ErrorState(props: {
|
||||
return (
|
||||
<div className={'flex flex-col space-y-4'}>
|
||||
<Alert variant={'destructive'}>
|
||||
<TriangleAlert className={'s-6'} />
|
||||
<ExclamationTriangleIcon className={'s-6'} />
|
||||
|
||||
<AlertTitle>
|
||||
<Trans i18nKey={'common.genericError'} />
|
||||
<Trans i18nKey={'common:genericError'} />
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription>{errorMessage}</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<Button onClick={props.onRetry} variant={'outline'}>
|
||||
<Trans i18nKey={'common.retry'} />
|
||||
<Trans i18nKey={'common:retry'} />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { RefinedPasswordSchema, refineRepeatPassword } from './password.schema';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { PasswordSchema } from './password.schema';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { RefinedPasswordSchema, refineRepeatPassword } from './password.schema';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
/**
|
||||
* Password requirements
|
||||
@@ -36,11 +36,13 @@ export function refineRepeatPassword(
|
||||
) {
|
||||
if (data.password !== data.repeatPassword) {
|
||||
ctx.addIssue({
|
||||
message: 'auth.errors.passwordsDoNotMatch',
|
||||
message: 'auth:errors.passwordsDoNotMatch',
|
||||
path: ['repeatPassword'],
|
||||
code: 'custom',
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function validatePassword(password: string, ctx: z.RefinementCtx) {
|
||||
@@ -50,7 +52,7 @@ function validatePassword(password: string, ctx: z.RefinementCtx) {
|
||||
|
||||
if (specialCharsCount < 1) {
|
||||
ctx.addIssue({
|
||||
message: 'auth.errors.minPasswordSpecialChars',
|
||||
message: 'auth:errors.minPasswordSpecialChars',
|
||||
code: 'custom',
|
||||
});
|
||||
}
|
||||
@@ -61,7 +63,7 @@ function validatePassword(password: string, ctx: z.RefinementCtx) {
|
||||
|
||||
if (numbersCount < 1) {
|
||||
ctx.addIssue({
|
||||
message: 'auth.errors.minPasswordNumbers',
|
||||
message: 'auth:errors.minPasswordNumbers',
|
||||
code: 'custom',
|
||||
});
|
||||
}
|
||||
@@ -70,9 +72,11 @@ function validatePassword(password: string, ctx: z.RefinementCtx) {
|
||||
if (requirements.uppercase) {
|
||||
if (!/[A-Z]/.test(password)) {
|
||||
ctx.addIssue({
|
||||
message: 'auth.errors.uppercasePassword',
|
||||
message: 'auth:errors.uppercasePassword',
|
||||
code: 'custom',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -23,9 +23,9 @@
|
||||
"@tanstack/react-query": "catalog:",
|
||||
"@types/react": "catalog:",
|
||||
"lucide-react": "catalog:",
|
||||
"next-intl": "catalog:",
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:"
|
||||
"react-dom": "catalog:",
|
||||
"react-i18next": "catalog:"
|
||||
},
|
||||
"prettier": "@kit/prettier-config",
|
||||
"typesVersions": {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { Bell, CircleAlert, Info, TriangleAlert, XIcon } from 'lucide-react';
|
||||
import { useLocale, useTranslations } from 'next-intl';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { If } from '@kit/ui/if';
|
||||
@@ -19,8 +19,7 @@ export function NotificationsPopover(params: {
|
||||
accountIds: string[];
|
||||
onClick?: (notification: Notification) => void;
|
||||
}) {
|
||||
const t = useTranslations();
|
||||
const locale = useLocale();
|
||||
const { i18n, t } = useTranslation();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [notifications, setNotifications] = useState<Notification[]>([]);
|
||||
@@ -54,7 +53,7 @@ export function NotificationsPopover(params: {
|
||||
(new Date().getTime() - date.getTime()) / (1000 * 60 * 60 * 24),
|
||||
);
|
||||
|
||||
const formatter = new Intl.RelativeTimeFormat(locale, {
|
||||
const formatter = new Intl.RelativeTimeFormat(i18n.language, {
|
||||
numeric: 'auto',
|
||||
});
|
||||
|
||||
@@ -62,7 +61,7 @@ export function NotificationsPopover(params: {
|
||||
time = Math.floor((new Date().getTime() - date.getTime()) / (1000 * 60));
|
||||
|
||||
if (time < 5) {
|
||||
return t('common.justNow');
|
||||
return t('common:justNow');
|
||||
}
|
||||
|
||||
if (time < 60) {
|
||||
@@ -111,42 +110,39 @@ export function NotificationsPopover(params: {
|
||||
|
||||
return (
|
||||
<Popover modal open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger
|
||||
render={
|
||||
<Button
|
||||
size="sm"
|
||||
className="text-secondary-foreground text-muted-foreground relative hover:bg-transparent"
|
||||
variant="ghost"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Bell className={'size-4 min-h-3 min-w-3'} />
|
||||
<PopoverTrigger asChild>
|
||||
<Button className={'relative h-9 w-9'} variant={'ghost'}>
|
||||
<Bell className={'min-h-4 min-w-4'} />
|
||||
|
||||
<span
|
||||
className={cn(
|
||||
`fade-in animate-in zoom-in absolute top-1 right-1 mt-0 flex h-3.5 w-3.5 items-center justify-center rounded-full bg-red-500 text-[0.65rem] text-white`,
|
||||
{
|
||||
hidden: !notifications.length,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{notifications.length}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
`fade-in animate-in zoom-in absolute top-1 right-1 mt-0 flex h-3.5 w-3.5 items-center justify-center rounded-full bg-red-500 text-[0.65rem] text-white`,
|
||||
{
|
||||
hidden: !notifications.length,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{notifications.length}
|
||||
</span>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverContent
|
||||
className={'flex w-full max-w-96 flex-col lg:min-w-64'}
|
||||
className={'flex w-full max-w-96 flex-col p-0 lg:min-w-64'}
|
||||
align={'start'}
|
||||
collisionPadding={20}
|
||||
sideOffset={10}
|
||||
>
|
||||
<div className={'flex items-center text-sm font-semibold'}>
|
||||
{t('common.notifications')}
|
||||
<div className={'flex items-center px-3 py-2 text-sm font-semibold'}>
|
||||
{t('common:notifications')}
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<If condition={!notifications.length}>
|
||||
<div className={'text-sm'}>{t('common.noNotifications')}</div>
|
||||
<div className={'px-3 py-2 text-sm'}>
|
||||
{t('common:noNotifications')}
|
||||
</div>
|
||||
</If>
|
||||
|
||||
<div
|
||||
|
||||
@@ -44,11 +44,10 @@
|
||||
"date-fns": "^4.1.0",
|
||||
"lucide-react": "catalog:",
|
||||
"next": "catalog:",
|
||||
"next-intl": "catalog:",
|
||||
"next-safe-action": "catalog:",
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
"react-hook-form": "catalog:",
|
||||
"react-i18next": "catalog:",
|
||||
"zod": "catalog:"
|
||||
},
|
||||
"prettier": "@kit/prettier-config",
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
'use client';
|
||||
|
||||
import { useMemo, useState, useTransition } from 'react';
|
||||
|
||||
import { isRedirectError } from 'next/dist/client/components/redirect-error';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -7,9 +16,24 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@kit/ui/dialog';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@kit/ui/form';
|
||||
import { If } from '@kit/ui/if';
|
||||
import { Input } from '@kit/ui/input';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { CreateTeamAccountForm } from './create-team-account-form';
|
||||
import {
|
||||
CreateTeamSchema,
|
||||
NON_LATIN_REGEX,
|
||||
} from '../schema/create-team.schema';
|
||||
import { createTeamAccountAction } from '../server/actions/create-team-account-server-actions';
|
||||
|
||||
export function CreateTeamAccountDialog(
|
||||
props: React.PropsWithChildren<{
|
||||
@@ -18,24 +42,171 @@ export function CreateTeamAccountDialog(
|
||||
}>,
|
||||
) {
|
||||
return (
|
||||
<Dialog
|
||||
open={props.isOpen}
|
||||
onOpenChange={props.setIsOpen}
|
||||
disablePointerDismissal
|
||||
>
|
||||
<DialogContent>
|
||||
<Dialog open={props.isOpen} onOpenChange={props.setIsOpen}>
|
||||
<DialogContent
|
||||
onEscapeKeyDown={(e) => e.preventDefault()}
|
||||
onInteractOutside={(e) => e.preventDefault()}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<Trans i18nKey={'teams.createTeamModalHeading'} />
|
||||
<Trans i18nKey={'teams:createTeamModalHeading'} />
|
||||
</DialogTitle>
|
||||
|
||||
<DialogDescription>
|
||||
<Trans i18nKey={'teams.createTeamModalDescription'} />
|
||||
<Trans i18nKey={'teams:createTeamModalDescription'} />
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<CreateTeamAccountForm onCancel={() => props.setIsOpen(false)} />
|
||||
<CreateOrganizationAccountForm onClose={() => props.setIsOpen(false)} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function CreateOrganizationAccountForm(props: { onClose: () => void }) {
|
||||
const [error, setError] = useState<{ message?: string } | undefined>();
|
||||
const [pending, startTransition] = useTransition();
|
||||
|
||||
const form = useForm({
|
||||
defaultValues: {
|
||||
name: '',
|
||||
slug: '',
|
||||
},
|
||||
resolver: zodResolver(CreateTeamSchema),
|
||||
});
|
||||
|
||||
const nameValue = useWatch({ control: form.control, name: 'name' });
|
||||
|
||||
const showSlugField = useMemo(
|
||||
() => NON_LATIN_REGEX.test(nameValue ?? ''),
|
||||
[nameValue],
|
||||
);
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
data-test={'create-team-form'}
|
||||
onSubmit={form.handleSubmit((data) => {
|
||||
startTransition(async () => {
|
||||
try {
|
||||
const result = await createTeamAccountAction(data);
|
||||
|
||||
if (result.error) {
|
||||
setError({ message: result.message });
|
||||
}
|
||||
} catch (e) {
|
||||
if (!isRedirectError(e)) {
|
||||
setError({});
|
||||
}
|
||||
}
|
||||
});
|
||||
})}
|
||||
>
|
||||
<div className={'flex flex-col space-y-4'}>
|
||||
<If condition={error}>
|
||||
<CreateOrganizationErrorAlert message={error?.message} />
|
||||
</If>
|
||||
|
||||
<FormField
|
||||
name={'name'}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans i18nKey={'teams:teamNameLabel'} />
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Input
|
||||
data-test={'team-name-input'}
|
||||
required
|
||||
minLength={2}
|
||||
maxLength={50}
|
||||
placeholder={''}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormDescription>
|
||||
<Trans i18nKey={'teams:teamNameDescription'} />
|
||||
</FormDescription>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<If condition={showSlugField}>
|
||||
<FormField
|
||||
name={'slug'}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans i18nKey={'teams:teamSlugLabel'} />
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Input
|
||||
data-test={'team-slug-input'}
|
||||
required
|
||||
minLength={2}
|
||||
maxLength={50}
|
||||
placeholder={'my-team'}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormDescription>
|
||||
<Trans i18nKey={'teams:teamSlugDescription'} />
|
||||
</FormDescription>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</If>
|
||||
|
||||
<div className={'flex justify-end space-x-2'}>
|
||||
<Button
|
||||
variant={'outline'}
|
||||
type={'button'}
|
||||
disabled={pending}
|
||||
onClick={props.onClose}
|
||||
>
|
||||
<Trans i18nKey={'common:cancel'} />
|
||||
</Button>
|
||||
|
||||
<Button data-test={'confirm-create-team-button'} disabled={pending}>
|
||||
{pending ? (
|
||||
<Trans i18nKey={'teams:creatingTeam'} />
|
||||
) : (
|
||||
<Trans i18nKey={'teams:createTeamSubmitLabel'} />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
function CreateOrganizationErrorAlert(props: { message?: string }) {
|
||||
return (
|
||||
<Alert variant={'destructive'}>
|
||||
<AlertTitle>
|
||||
<Trans i18nKey={'teams:createTeamErrorHeading'} />
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
{props.message ? (
|
||||
<Trans i18nKey={props.message} defaults={props.message} />
|
||||
) : (
|
||||
<Trans i18nKey={'teams:createTeamErrorMessage'} />
|
||||
)}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,183 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@kit/ui/form';
|
||||
import { If } from '@kit/ui/if';
|
||||
import { Input } from '@kit/ui/input';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import {
|
||||
CreateTeamSchema,
|
||||
NON_LATIN_REGEX,
|
||||
} from '../schema/create-team.schema';
|
||||
import { createTeamAccountAction } from '../server/actions/create-team-account-server-actions';
|
||||
|
||||
export function CreateTeamAccountForm(props: {
|
||||
onCancel?: () => void;
|
||||
submitLabel?: string;
|
||||
}) {
|
||||
const [error, setError] = useState<{ message?: string } | undefined>();
|
||||
|
||||
const { execute, isPending } = useAction(createTeamAccountAction, {
|
||||
onExecute: () => {
|
||||
setError(undefined);
|
||||
},
|
||||
onSuccess: ({ data }) => {
|
||||
if (data?.error) {
|
||||
setError({ message: data.message });
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
setError({});
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm({
|
||||
defaultValues: {
|
||||
name: '',
|
||||
slug: '',
|
||||
},
|
||||
resolver: zodResolver(CreateTeamSchema),
|
||||
});
|
||||
|
||||
const nameValue = useWatch({ control: form.control, name: 'name' });
|
||||
|
||||
const showSlugField = NON_LATIN_REGEX.test(nameValue ?? '');
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
data-test={'create-team-form'}
|
||||
onSubmit={form.handleSubmit((data) => execute(data))}
|
||||
>
|
||||
<div className={'flex flex-col space-y-4'}>
|
||||
<If condition={error}>
|
||||
<CreateTeamAccountErrorAlert message={error?.message} />
|
||||
</If>
|
||||
|
||||
<FormField
|
||||
name={'name'}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans i18nKey={'teams.teamNameLabel'} />
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Input
|
||||
data-test={'team-name-input'}
|
||||
required
|
||||
minLength={2}
|
||||
maxLength={50}
|
||||
placeholder={''}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormDescription>
|
||||
<Trans i18nKey={'teams.teamNameDescription'} />
|
||||
</FormDescription>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<If condition={showSlugField}>
|
||||
<FormField
|
||||
name={'slug'}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans i18nKey={'teams.teamSlugLabel'} />
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Input
|
||||
data-test={'team-slug-input'}
|
||||
required
|
||||
minLength={2}
|
||||
maxLength={50}
|
||||
placeholder={'my-team'}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormDescription>
|
||||
<Trans i18nKey={'teams.teamSlugDescription'} />
|
||||
</FormDescription>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</If>
|
||||
|
||||
<div className={'flex justify-end space-x-2'}>
|
||||
<If condition={!!props.onCancel}>
|
||||
<Button
|
||||
variant={'outline'}
|
||||
type={'button'}
|
||||
disabled={isPending}
|
||||
onClick={props.onCancel}
|
||||
>
|
||||
<Trans i18nKey={'common.cancel'} />
|
||||
</Button>
|
||||
</If>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
data-test={'confirm-create-team-button'}
|
||||
disabled={isPending}
|
||||
>
|
||||
{isPending ? (
|
||||
<Trans i18nKey={'teams.creatingTeam'} />
|
||||
) : (
|
||||
<Trans
|
||||
i18nKey={props.submitLabel ?? 'teams.createTeamSubmitLabel'}
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
function CreateTeamAccountErrorAlert(props: { message?: string }) {
|
||||
return (
|
||||
<Alert variant={'destructive'}>
|
||||
<AlertTitle>
|
||||
<Trans i18nKey={'teams.createTeamErrorHeading'} />
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
{props.message ? (
|
||||
<Trans i18nKey={props.message} defaults={props.message} />
|
||||
) : (
|
||||
<Trans i18nKey={'teams.createTeamErrorMessage'} />
|
||||
)}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
@@ -5,5 +5,4 @@ export * from './invitations/account-invitations-table';
|
||||
export * from './settings/team-account-settings-container';
|
||||
export * from './invitations/accept-invitation-container';
|
||||
export * from './create-team-account-dialog';
|
||||
export * from './create-team-account-form';
|
||||
export * from './team-account-workspace-context';
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import Image from 'next/image';
|
||||
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { Heading } from '@kit/ui/heading';
|
||||
import { If } from '@kit/ui/if';
|
||||
import { Separator } from '@kit/ui/separator';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { acceptInvitationAction } from '../../server/actions/team-invitations-server-actions';
|
||||
import { InvitationSubmitButton } from './invitation-submit-button';
|
||||
import { SignOutInvitationButton } from './sign-out-invitation-button';
|
||||
|
||||
export function AcceptInvitationContainer(props: {
|
||||
@@ -32,13 +28,11 @@ export function AcceptInvitationContainer(props: {
|
||||
nextPath: string;
|
||||
};
|
||||
}) {
|
||||
const { execute, isPending } = useAction(acceptInvitationAction);
|
||||
|
||||
return (
|
||||
<div className={'flex flex-col items-center space-y-4'}>
|
||||
<Heading className={'text-center'} level={4}>
|
||||
<Trans
|
||||
i18nKey={'teams.acceptInvitationHeading'}
|
||||
i18nKey={'teams:acceptInvitationHeading'}
|
||||
values={{
|
||||
accountName: props.invitation.account.name,
|
||||
}}
|
||||
@@ -59,7 +53,7 @@ export function AcceptInvitationContainer(props: {
|
||||
|
||||
<div className={'text-muted-foreground text-center text-sm'}>
|
||||
<Trans
|
||||
i18nKey={'teams.acceptInvitationDescription'}
|
||||
i18nKey={'teams:acceptInvitationDescription'}
|
||||
values={{
|
||||
accountName: props.invitation.account.name,
|
||||
}}
|
||||
@@ -70,24 +64,20 @@ export function AcceptInvitationContainer(props: {
|
||||
<form
|
||||
data-test={'join-team-form'}
|
||||
className={'w-full'}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
|
||||
execute({
|
||||
inviteToken: props.inviteToken,
|
||||
nextPath: props.paths.nextPath,
|
||||
});
|
||||
}}
|
||||
action={acceptInvitationAction}
|
||||
>
|
||||
<Button type={'submit'} className={'w-full'} disabled={isPending}>
|
||||
<Trans
|
||||
i18nKey={isPending ? 'teams.joiningTeam' : 'teams.continueAs'}
|
||||
values={{
|
||||
accountName: props.invitation.account.name,
|
||||
email: props.email,
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
<input type="hidden" name={'inviteToken'} value={props.inviteToken} />
|
||||
|
||||
<input
|
||||
type={'hidden'}
|
||||
name={'nextPath'}
|
||||
value={props.paths.nextPath}
|
||||
/>
|
||||
|
||||
<InvitationSubmitButton
|
||||
email={props.email}
|
||||
accountName={props.invitation.account.name}
|
||||
/>
|
||||
</form>
|
||||
|
||||
<Separator />
|
||||
@@ -95,7 +85,7 @@ export function AcceptInvitationContainer(props: {
|
||||
<SignOutInvitationButton nextPath={props.paths.signOutNext} />
|
||||
|
||||
<span className={'text-muted-foreground text-center text-xs'}>
|
||||
<Trans i18nKey={'teams.signInWithDifferentAccountDescription'} />
|
||||
<Trans i18nKey={'teams:signInWithDifferentAccountDescription'} />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useMemo, useState } from 'react';
|
||||
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import { Ellipsis } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Database } from '@kit/supabase/database';
|
||||
import { Badge } from '@kit/ui/badge';
|
||||
@@ -43,7 +43,7 @@ export function AccountInvitationsTable({
|
||||
invitations,
|
||||
permissions,
|
||||
}: AccountInvitationsTableProps) {
|
||||
const t = useTranslations('teams');
|
||||
const { t } = useTranslation('teams');
|
||||
const [search, setSearch] = useState('');
|
||||
const columns = useGetColumns(permissions);
|
||||
|
||||
@@ -82,7 +82,7 @@ function useGetColumns(permissions: {
|
||||
canRemoveInvitation: boolean;
|
||||
currentUserRoleHierarchy: number;
|
||||
}): ColumnDef<Invitations[0]>[] {
|
||||
const t = useTranslations('teams');
|
||||
const { t } = useTranslation('teams');
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
@@ -96,7 +96,7 @@ function useGetColumns(permissions: {
|
||||
return (
|
||||
<span
|
||||
data-test={'invitation-email'}
|
||||
className={'flex items-center gap-x-2 text-left'}
|
||||
className={'flex items-center space-x-4 text-left'}
|
||||
>
|
||||
<span>
|
||||
<ProfileAvatar text={email} />
|
||||
@@ -172,21 +172,19 @@ function ActionsDropdown({
|
||||
return (
|
||||
<>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<Button variant={'ghost'} size={'icon'}>
|
||||
<Ellipsis className={'h-5 w-5'} />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant={'ghost'} size={'icon'}>
|
||||
<Ellipsis className={'h-5 w-5'} />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent className="min-w-52">
|
||||
<DropdownMenuContent>
|
||||
<If condition={permissions.canUpdateInvitation}>
|
||||
<DropdownMenuItem
|
||||
data-test={'update-invitation-trigger'}
|
||||
onClick={() => setIsUpdatingRole(true)}
|
||||
>
|
||||
<Trans i18nKey={'teams.updateInvitation'} />
|
||||
<Trans i18nKey={'teams:updateInvitation'} />
|
||||
</DropdownMenuItem>
|
||||
|
||||
<If condition={getIsInviteExpired(invitation.expires_at)}>
|
||||
@@ -194,7 +192,7 @@ function ActionsDropdown({
|
||||
data-test={'renew-invitation-trigger'}
|
||||
onClick={() => setIsRenewingInvite(true)}
|
||||
>
|
||||
<Trans i18nKey={'teams.renewInvitation'} />
|
||||
<Trans i18nKey={'teams:renewInvitation'} />
|
||||
</DropdownMenuItem>
|
||||
</If>
|
||||
</If>
|
||||
@@ -204,7 +202,7 @@ function ActionsDropdown({
|
||||
data-test={'remove-invitation-trigger'}
|
||||
onClick={() => setIsDeletingInvite(true)}
|
||||
>
|
||||
<Trans i18nKey={'teams.removeInvitation'} />
|
||||
<Trans i18nKey={'teams:removeInvitation'} />
|
||||
</DropdownMenuItem>
|
||||
</If>
|
||||
</DropdownMenuContent>
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
'use client';
|
||||
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { useState, useTransition } from 'react';
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||
import {
|
||||
@@ -32,11 +30,11 @@ export function DeleteInvitationDialog({
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
<Trans i18nKey="teams.deleteInvitation" />
|
||||
<Trans i18nKey="team:deleteInvitation" />
|
||||
</AlertDialogTitle>
|
||||
|
||||
<AlertDialogDescription>
|
||||
<Trans i18nKey="teams.deleteInvitationDialogDescription" />
|
||||
<Trans i18nKey="team:deleteInvitationDialogDescription" />
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
|
||||
@@ -56,34 +54,43 @@ function DeleteInvitationForm({
|
||||
invitationId: number;
|
||||
setIsOpen: (isOpen: boolean) => void;
|
||||
}) {
|
||||
const { execute, isPending, hasErrored } = useAction(deleteInvitationAction, {
|
||||
onSuccess: () => setIsOpen(false),
|
||||
});
|
||||
const [isSubmitting, startTransition] = useTransition();
|
||||
const [error, setError] = useState<boolean>();
|
||||
|
||||
const onInvitationRemoved = () => {
|
||||
startTransition(async () => {
|
||||
try {
|
||||
await deleteInvitationAction({ invitationId });
|
||||
|
||||
setIsOpen(false);
|
||||
} catch {
|
||||
setError(true);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
data-test={'delete-invitation-form'}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
execute({ invitationId });
|
||||
}}
|
||||
>
|
||||
<form data-test={'delete-invitation-form'} action={onInvitationRemoved}>
|
||||
<div className={'flex flex-col space-y-6'}>
|
||||
<p className={'text-muted-foreground text-sm'}>
|
||||
<Trans i18nKey={'common.modalConfirmationQuestion'} />
|
||||
<Trans i18nKey={'common:modalConfirmationQuestion'} />
|
||||
</p>
|
||||
|
||||
<If condition={hasErrored}>
|
||||
<If condition={error}>
|
||||
<RemoveInvitationErrorAlert />
|
||||
</If>
|
||||
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>
|
||||
<Trans i18nKey={'common.cancel'} />
|
||||
<Trans i18nKey={'common:cancel'} />
|
||||
</AlertDialogCancel>
|
||||
|
||||
<Button type={'submit'} variant={'destructive'} disabled={isPending}>
|
||||
<Trans i18nKey={'teams.deleteInvitation'} />
|
||||
<Button
|
||||
type={'submit'}
|
||||
variant={'destructive'}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<Trans i18nKey={'teams:deleteInvitation'} />
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</div>
|
||||
@@ -95,11 +102,11 @@ function RemoveInvitationErrorAlert() {
|
||||
return (
|
||||
<Alert variant={'destructive'}>
|
||||
<AlertTitle>
|
||||
<Trans i18nKey={'teams.deleteInvitationErrorTitle'} />
|
||||
<Trans i18nKey={'teams:deleteInvitationErrorTitle'} />
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
<Trans i18nKey={'teams.deleteInvitationErrorMessage'} />
|
||||
<Trans i18nKey={'teams:deleteInvitationErrorMessage'} />
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
|
||||
@@ -14,7 +14,7 @@ export function InvitationSubmitButton(props: {
|
||||
return (
|
||||
<Button type={'submit'} className={'w-full'} disabled={pending}>
|
||||
<Trans
|
||||
i18nKey={pending ? 'teams.joiningTeam' : 'teams.continueAs'}
|
||||
i18nKey={pending ? 'teams:joiningTeam' : 'teams:continueAs'}
|
||||
values={{
|
||||
accountName: props.accountName,
|
||||
email: props.email,
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
'use client';
|
||||
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { useState, useTransition } from 'react';
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||
import {
|
||||
@@ -34,12 +32,12 @@ export function RenewInvitationDialog({
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
<Trans i18nKey="team.renewInvitation" />
|
||||
<Trans i18nKey="team:renewInvitation" />
|
||||
</AlertDialogTitle>
|
||||
|
||||
<AlertDialogDescription>
|
||||
<Trans
|
||||
i18nKey="team.renewInvitationDialogDescription"
|
||||
i18nKey="team:renewInvitationDialogDescription"
|
||||
values={{ email }}
|
||||
/>
|
||||
</AlertDialogDescription>
|
||||
@@ -61,33 +59,42 @@ function RenewInvitationForm({
|
||||
invitationId: number;
|
||||
setIsOpen: (isOpen: boolean) => void;
|
||||
}) {
|
||||
const { execute, isPending, hasErrored } = useAction(renewInvitationAction, {
|
||||
onSuccess: () => setIsOpen(false),
|
||||
});
|
||||
const [isSubmitting, startTransition] = useTransition();
|
||||
const [error, setError] = useState<boolean>();
|
||||
|
||||
const inInvitationRenewed = () => {
|
||||
startTransition(async () => {
|
||||
try {
|
||||
await renewInvitationAction({ invitationId });
|
||||
|
||||
setIsOpen(false);
|
||||
} catch {
|
||||
setError(true);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
execute({ invitationId });
|
||||
}}
|
||||
>
|
||||
<form action={inInvitationRenewed}>
|
||||
<div className={'flex flex-col space-y-6'}>
|
||||
<p className={'text-muted-foreground text-sm'}>
|
||||
<Trans i18nKey={'common.modalConfirmationQuestion'} />
|
||||
<Trans i18nKey={'common:modalConfirmationQuestion'} />
|
||||
</p>
|
||||
|
||||
<If condition={hasErrored}>
|
||||
<If condition={error}>
|
||||
<RenewInvitationErrorAlert />
|
||||
</If>
|
||||
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>
|
||||
<Trans i18nKey={'common.cancel'} />
|
||||
<Trans i18nKey={'common:cancel'} />
|
||||
</AlertDialogCancel>
|
||||
|
||||
<Button data-test={'confirm-renew-invitation'} disabled={isPending}>
|
||||
<Trans i18nKey={'teams.renewInvitation'} />
|
||||
<Button
|
||||
data-test={'confirm-renew-invitation'}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<Trans i18nKey={'teams:renewInvitation'} />
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</div>
|
||||
@@ -99,11 +106,11 @@ function RenewInvitationErrorAlert() {
|
||||
return (
|
||||
<Alert variant={'destructive'}>
|
||||
<AlertTitle>
|
||||
<Trans i18nKey={'teams.renewInvitationErrorTitle'} />
|
||||
<Trans i18nKey={'teams:renewInvitationErrorTitle'} />
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
<Trans i18nKey={'teams.renewInvitationErrorDescription'} />
|
||||
<Trans i18nKey={'teams:renewInvitationErrorDescription'} />
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
|
||||
@@ -24,7 +24,7 @@ export function SignOutInvitationButton(
|
||||
window.location.assign(safePath);
|
||||
}}
|
||||
>
|
||||
<Trans i18nKey={'teams.signInWithDifferentAccount'} />
|
||||
<Trans i18nKey={'teams:signInWithDifferentAccount'} />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
'use client';
|
||||
import { useState, useTransition } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||
import { Button } from '@kit/ui/button';
|
||||
@@ -51,11 +50,11 @@ export function UpdateInvitationDialog({
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<Trans i18nKey={'teams.updateMemberRoleModalHeading'} />
|
||||
<Trans i18nKey={'teams:updateMemberRoleModalHeading'} />
|
||||
</DialogTitle>
|
||||
|
||||
<DialogDescription>
|
||||
<Trans i18nKey={'teams.updateMemberRoleModalDescription'} />
|
||||
<Trans i18nKey={'teams:updateMemberRoleModalDescription'} />
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -81,11 +80,24 @@ function UpdateInvitationForm({
|
||||
userRoleHierarchy: number;
|
||||
setIsOpen: (isOpen: boolean) => void;
|
||||
}>) {
|
||||
const t = useTranslations('teams');
|
||||
const { t } = useTranslation('teams');
|
||||
const [pending, startTransition] = useTransition();
|
||||
const [error, setError] = useState<boolean>();
|
||||
|
||||
const { execute, isPending, hasErrored } = useAction(updateInvitationAction, {
|
||||
onSuccess: () => setIsOpen(false),
|
||||
});
|
||||
const onSubmit = ({ role }: { role: Role }) => {
|
||||
startTransition(async () => {
|
||||
try {
|
||||
await updateInvitationAction({
|
||||
invitationId,
|
||||
role,
|
||||
});
|
||||
|
||||
setIsOpen(false);
|
||||
} catch {
|
||||
setError(true);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(
|
||||
@@ -110,12 +122,10 @@ function UpdateInvitationForm({
|
||||
<Form {...form}>
|
||||
<form
|
||||
data-test={'update-invitation-form'}
|
||||
onSubmit={form.handleSubmit(({ role }) => {
|
||||
execute({ invitationId, role });
|
||||
})}
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className={'flex flex-col space-y-6'}
|
||||
>
|
||||
<If condition={hasErrored}>
|
||||
<If condition={error}>
|
||||
<UpdateRoleErrorAlert />
|
||||
</If>
|
||||
|
||||
@@ -125,7 +135,7 @@ function UpdateInvitationForm({
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans i18nKey={'teams.roleLabel'} />
|
||||
<Trans i18nKey={'teams:roleLabel'} />
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
@@ -135,18 +145,16 @@ function UpdateInvitationForm({
|
||||
roles={roles}
|
||||
currentUserRole={userRole}
|
||||
value={field.value}
|
||||
onChange={(newRole) => {
|
||||
if (newRole) {
|
||||
form.setValue(field.name, newRole);
|
||||
}
|
||||
}}
|
||||
onChange={(newRole) =>
|
||||
form.setValue(field.name, newRole)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</RolesDataProvider>
|
||||
</FormControl>
|
||||
|
||||
<FormDescription>
|
||||
<Trans i18nKey={'teams.updateRoleDescription'} />
|
||||
<Trans i18nKey={'teams:updateRoleDescription'} />
|
||||
</FormDescription>
|
||||
|
||||
<FormMessage />
|
||||
@@ -155,8 +163,8 @@ function UpdateInvitationForm({
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button type={'submit'} disabled={isPending}>
|
||||
<Trans i18nKey={'teams.updateRoleSubmitLabel'} />
|
||||
<Button type={'submit'} disabled={pending}>
|
||||
<Trans i18nKey={'teams:updateRoleSubmitLabel'} />
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
@@ -167,11 +175,11 @@ function UpdateRoleErrorAlert() {
|
||||
return (
|
||||
<Alert variant={'destructive'}>
|
||||
<AlertTitle>
|
||||
<Trans i18nKey={'teams.updateRoleErrorHeading'} />
|
||||
<Trans i18nKey={'teams:updateRoleErrorHeading'} />
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
<Trans i18nKey={'teams.updateRoleErrorMessage'} />
|
||||
<Trans i18nKey={'teams:updateRoleErrorMessage'} />
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useMemo, useState } from 'react';
|
||||
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import { Ellipsis } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Database } from '@kit/supabase/database';
|
||||
import { Badge } from '@kit/ui/badge';
|
||||
@@ -53,7 +53,7 @@ export function AccountMembersTable({
|
||||
canManageRoles,
|
||||
}: AccountMembersTableProps) {
|
||||
const [search, setSearch] = useState('');
|
||||
const t = useTranslations('teams');
|
||||
const { t } = useTranslation('teams');
|
||||
|
||||
const permissions = {
|
||||
canUpdateRole: (targetRole: number) => {
|
||||
@@ -123,7 +123,7 @@ function useGetColumns(
|
||||
currentRoleHierarchy: number;
|
||||
},
|
||||
): ColumnDef<Members[0]>[] {
|
||||
const t = useTranslations('teams');
|
||||
const { t } = useTranslation('teams');
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
@@ -136,7 +136,7 @@ function useGetColumns(
|
||||
const isSelf = member.user_id === params.currentUserId;
|
||||
|
||||
return (
|
||||
<span className={'flex items-center gap-x-2 text-left'}>
|
||||
<span className={'flex items-center space-x-4 text-left'}>
|
||||
<span>
|
||||
<ProfileAvatar
|
||||
displayName={displayName}
|
||||
@@ -144,13 +144,11 @@ function useGetColumns(
|
||||
/>
|
||||
</span>
|
||||
|
||||
<span className={'flex items-center gap-x-2'}>
|
||||
<span>{displayName}</span>
|
||||
<span>{displayName}</span>
|
||||
|
||||
<If condition={isSelf}>
|
||||
<Badge variant={'secondary'}>{t('youLabel')}</Badge>
|
||||
</If>
|
||||
</span>
|
||||
<If condition={isSelf}>
|
||||
<Badge variant={'outline'}>{t('youLabel')}</Badge>
|
||||
</If>
|
||||
</span>
|
||||
);
|
||||
},
|
||||
@@ -173,7 +171,13 @@ function useGetColumns(
|
||||
<RoleBadge role={role} />
|
||||
|
||||
<If condition={isPrimaryOwner}>
|
||||
<Badge variant={'warning'}>{t('primaryOwnerLabel')}</Badge>
|
||||
<span
|
||||
className={
|
||||
'rounded-md bg-yellow-400 px-2.5 py-1 text-xs font-medium dark:text-black'
|
||||
}
|
||||
>
|
||||
{t('primaryOwnerLabel')}
|
||||
</span>
|
||||
</If>
|
||||
</span>
|
||||
);
|
||||
@@ -219,10 +223,6 @@ function ActionsDropdown({
|
||||
const isCurrentUser = member.user_id === currentUserId;
|
||||
const isPrimaryOwner = member.primary_owner_user_id === member.user_id;
|
||||
|
||||
const [activeDialog, setActiveDialog] = useState<
|
||||
'updateRole' | 'transferOwnership' | 'removeMember' | null
|
||||
>(null);
|
||||
|
||||
if (isCurrentUser || isPrimaryOwner) {
|
||||
return null;
|
||||
}
|
||||
@@ -246,66 +246,50 @@ function ActionsDropdown({
|
||||
return (
|
||||
<>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<Button variant={'ghost'} size={'icon'}>
|
||||
<Ellipsis className={'h-5 w-5'} />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant={'ghost'} size={'icon'}>
|
||||
<Ellipsis className={'h-5 w-5'} />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent className={'min-w-52'}>
|
||||
<DropdownMenuContent>
|
||||
<If condition={canUpdateRole}>
|
||||
<DropdownMenuItem onClick={() => setActiveDialog('updateRole')}>
|
||||
<Trans i18nKey={'teams.updateRole'} />
|
||||
</DropdownMenuItem>
|
||||
<UpdateMemberRoleDialog
|
||||
userId={member.user_id}
|
||||
userRole={member.role}
|
||||
teamAccountId={currentTeamAccountId}
|
||||
userRoleHierarchy={currentRoleHierarchy}
|
||||
>
|
||||
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
|
||||
<Trans i18nKey={'teams:updateRole'} />
|
||||
</DropdownMenuItem>
|
||||
</UpdateMemberRoleDialog>
|
||||
</If>
|
||||
|
||||
<If condition={permissions.canTransferOwnership}>
|
||||
<DropdownMenuItem
|
||||
onClick={() => setActiveDialog('transferOwnership')}
|
||||
<TransferOwnershipDialog
|
||||
targetDisplayName={member.name ?? member.email}
|
||||
accountId={member.account_id}
|
||||
userId={member.user_id}
|
||||
>
|
||||
<Trans i18nKey={'teams.transferOwnership'} />
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
|
||||
<Trans i18nKey={'teams:transferOwnership'} />
|
||||
</DropdownMenuItem>
|
||||
</TransferOwnershipDialog>
|
||||
</If>
|
||||
|
||||
<If condition={canRemoveFromAccount}>
|
||||
<DropdownMenuItem onClick={() => setActiveDialog('removeMember')}>
|
||||
<Trans i18nKey={'teams.removeMember'} />
|
||||
</DropdownMenuItem>
|
||||
<RemoveMemberDialog
|
||||
teamAccountId={currentTeamAccountId}
|
||||
userId={member.user_id}
|
||||
>
|
||||
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
|
||||
<Trans i18nKey={'teams:removeMember'} />
|
||||
</DropdownMenuItem>
|
||||
</RemoveMemberDialog>
|
||||
</If>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{activeDialog === 'updateRole' && (
|
||||
<UpdateMemberRoleDialog
|
||||
open
|
||||
onOpenChange={(open) => !open && setActiveDialog(null)}
|
||||
userId={member.user_id}
|
||||
userRole={member.role}
|
||||
teamAccountId={currentTeamAccountId}
|
||||
userRoleHierarchy={currentRoleHierarchy}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeDialog === 'transferOwnership' && (
|
||||
<TransferOwnershipDialog
|
||||
open
|
||||
onOpenChange={(open) => !open && setActiveDialog(null)}
|
||||
targetDisplayName={member.name ?? member.email}
|
||||
accountId={member.account_id}
|
||||
userId={member.user_id}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeDialog === 'removeMember' && (
|
||||
<RemoveMemberDialog
|
||||
open
|
||||
onOpenChange={(open) => !open && setActiveDialog(null)}
|
||||
teamAccountId={currentTeamAccountId}
|
||||
userId={member.user_id}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useState, useTransition } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Mail, Plus, X } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { useFieldArray, useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Alert, AlertDescription } from '@kit/ui/alert';
|
||||
import { Button } from '@kit/ui/button';
|
||||
@@ -65,24 +64,9 @@ export function InviteMembersDialogContainer({
|
||||
accountSlug: string;
|
||||
userRoleHierarchy: number;
|
||||
}>) {
|
||||
const [pending, startTransition] = useTransition();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const t = useTranslations('teams');
|
||||
|
||||
const { execute, isPending } = useAction(createInvitationsAction, {
|
||||
onSuccess: ({ data }) => {
|
||||
if (data?.success) {
|
||||
toast.success(t('inviteMembersSuccessMessage'));
|
||||
} else {
|
||||
toast.error(t('inviteMembersErrorMessage'));
|
||||
}
|
||||
|
||||
setIsOpen(false);
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(t('inviteMembersErrorMessage'));
|
||||
setIsOpen(false);
|
||||
},
|
||||
});
|
||||
const { t } = useTranslation('teams');
|
||||
|
||||
// Evaluate policies when dialog is open
|
||||
const {
|
||||
@@ -92,17 +76,17 @@ export function InviteMembersDialogContainer({
|
||||
} = useFetchInvitationsPolicies({ accountSlug, isOpen });
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen} disablePointerDismissal>
|
||||
<DialogTrigger render={children as React.ReactElement} />
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen} modal>
|
||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||
|
||||
<DialogContent>
|
||||
<DialogContent onInteractOutside={(e) => e.preventDefault()}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<Trans i18nKey={'teams.inviteMembersHeading'} />
|
||||
<Trans i18nKey={'teams:inviteMembersHeading'} />
|
||||
</DialogTitle>
|
||||
|
||||
<DialogDescription>
|
||||
<Trans i18nKey={'teams.inviteMembersDescription'} />
|
||||
<Trans i18nKey={'teams:inviteMembersDescription'} />
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -111,7 +95,7 @@ export function InviteMembersDialogContainer({
|
||||
<Spinner className="h-6 w-6" />
|
||||
|
||||
<span className="text-muted-foreground text-sm">
|
||||
<Trans i18nKey="teams.checkingPolicies" />
|
||||
<Trans i18nKey="teams:checkingPolicies" />
|
||||
</span>
|
||||
</div>
|
||||
</If>
|
||||
@@ -120,7 +104,7 @@ export function InviteMembersDialogContainer({
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription>
|
||||
<Trans
|
||||
i18nKey="teams.policyCheckError"
|
||||
i18nKey="teams:policyCheckError"
|
||||
values={{ error: policiesError?.message }}
|
||||
/>
|
||||
</AlertDescription>
|
||||
@@ -142,12 +126,28 @@ export function InviteMembersDialogContainer({
|
||||
<RolesDataProvider maxRoleHierarchy={userRoleHierarchy}>
|
||||
{(roles) => (
|
||||
<InviteMembersForm
|
||||
pending={isPending}
|
||||
pending={pending}
|
||||
roles={roles}
|
||||
onSubmit={(data) => {
|
||||
execute({
|
||||
accountSlug,
|
||||
invitations: data.invitations,
|
||||
startTransition(async () => {
|
||||
const toastId = toast.loading(t('invitingMembers'));
|
||||
|
||||
const result = await createInvitationsAction({
|
||||
accountSlug,
|
||||
invitations: data.invitations,
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
toast.success(t('inviteMembersSuccessMessage'), {
|
||||
id: toastId,
|
||||
});
|
||||
} else {
|
||||
toast.error(t('inviteMembersErrorMessage'), {
|
||||
id: toastId,
|
||||
});
|
||||
}
|
||||
|
||||
setIsOpen(false);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
@@ -168,7 +168,7 @@ function InviteMembersForm({
|
||||
pending: boolean;
|
||||
roles: string[];
|
||||
}) {
|
||||
const t = useTranslations('teams');
|
||||
const { t } = useTranslation('teams');
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(InviteMembersSchema),
|
||||
@@ -237,9 +237,7 @@ function InviteMembersForm({
|
||||
roles={roles}
|
||||
value={field.value}
|
||||
onChange={(role) => {
|
||||
if (role) {
|
||||
form.setValue(field.name, role);
|
||||
}
|
||||
form.setValue(field.name, role);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
@@ -253,24 +251,22 @@ function InviteMembersForm({
|
||||
<div className={'flex items-end justify-end'}>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<Button
|
||||
variant={'ghost'}
|
||||
size={'icon'}
|
||||
type={'button'}
|
||||
disabled={fieldArray.fields.length <= 1}
|
||||
data-test={'remove-invite-button'}
|
||||
aria-label={t('removeInviteButtonLabel')}
|
||||
onClick={() => {
|
||||
fieldArray.remove(index);
|
||||
form.clearErrors(emailInputName);
|
||||
}}
|
||||
>
|
||||
<X className={'h-4'} />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant={'ghost'}
|
||||
size={'icon'}
|
||||
type={'button'}
|
||||
disabled={fieldArray.fields.length <= 1}
|
||||
data-test={'remove-invite-button'}
|
||||
aria-label={t('removeInviteButtonLabel')}
|
||||
onClick={() => {
|
||||
fieldArray.remove(index);
|
||||
form.clearErrors(emailInputName);
|
||||
}}
|
||||
>
|
||||
<X className={'h-4'} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent>
|
||||
{t('removeInviteButtonLabel')}
|
||||
@@ -298,7 +294,7 @@ function InviteMembersForm({
|
||||
<Plus className={'mr-1 h-3'} />
|
||||
|
||||
<span>
|
||||
<Trans i18nKey={'teams.addAnotherMemberButtonLabel'} />
|
||||
<Trans i18nKey={'teams:addAnotherMemberButtonLabel'} />
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
@@ -309,8 +305,8 @@ function InviteMembersForm({
|
||||
<Trans
|
||||
i18nKey={
|
||||
pending
|
||||
? 'teams.invitingMembers'
|
||||
: 'teams.inviteMembersButtonLabel'
|
||||
? 'teams:invitingMembers'
|
||||
: 'teams:inviteMembersButtonLabel'
|
||||
}
|
||||
/>
|
||||
</Button>
|
||||
|
||||
@@ -19,7 +19,7 @@ export function MembershipRoleSelector({
|
||||
roles: Role[];
|
||||
value: Role;
|
||||
currentUserRole?: Role;
|
||||
onChange: (role: Role | null) => unknown;
|
||||
onChange: (role: Role) => unknown;
|
||||
triggerClassName?: string;
|
||||
}) {
|
||||
return (
|
||||
@@ -28,15 +28,7 @@ export function MembershipRoleSelector({
|
||||
className={triggerClassName}
|
||||
data-test={'role-selector-trigger'}
|
||||
>
|
||||
<SelectValue>
|
||||
{(value) =>
|
||||
value ? (
|
||||
<Trans i18nKey={`common.roles.${value}.label`} defaults={value} />
|
||||
) : (
|
||||
''
|
||||
)
|
||||
}
|
||||
</SelectValue>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
@@ -49,7 +41,7 @@ export function MembershipRoleSelector({
|
||||
value={role}
|
||||
>
|
||||
<span className={'text-sm capitalize'}>
|
||||
<Trans i18nKey={`common.roles.${role}.label`} defaults={role} />
|
||||
<Trans i18nKey={`common:roles.${role}.label`} defaults={role} />
|
||||
</span>
|
||||
</SelectItem>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
'use client';
|
||||
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { useState, useTransition } from 'react';
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||
import {
|
||||
@@ -11,6 +9,7 @@ import {
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from '@kit/ui/alert-dialog';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { If } from '@kit/ui/if';
|
||||
@@ -19,34 +18,29 @@ import { Trans } from '@kit/ui/trans';
|
||||
import { removeMemberFromAccountAction } from '../../server/actions/team-members-server-actions';
|
||||
|
||||
export function RemoveMemberDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
teamAccountId,
|
||||
userId,
|
||||
}: {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
children,
|
||||
}: React.PropsWithChildren<{
|
||||
teamAccountId: string;
|
||||
userId: string;
|
||||
}) {
|
||||
}>) {
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>{children}</AlertDialogTrigger>
|
||||
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
<Trans i18nKey="teams.removeMemberModalHeading" />
|
||||
<Trans i18nKey="teamS:removeMemberModalHeading" />
|
||||
</AlertDialogTitle>
|
||||
|
||||
<AlertDialogDescription>
|
||||
<Trans i18nKey={'teams.removeMemberModalDescription'} />
|
||||
<Trans i18nKey={'teams:removeMemberModalDescription'} />
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
|
||||
<RemoveMemberForm
|
||||
accountId={teamAccountId}
|
||||
userId={userId}
|
||||
onSuccess={() => onOpenChange(false)}
|
||||
/>
|
||||
<RemoveMemberForm accountId={teamAccountId} userId={userId} />
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
@@ -55,46 +49,45 @@ export function RemoveMemberDialog({
|
||||
function RemoveMemberForm({
|
||||
accountId,
|
||||
userId,
|
||||
onSuccess,
|
||||
}: {
|
||||
accountId: string;
|
||||
userId: string;
|
||||
onSuccess: () => void;
|
||||
}) {
|
||||
const { execute, isPending, hasErrored } = useAction(
|
||||
removeMemberFromAccountAction,
|
||||
{
|
||||
onSuccess: () => onSuccess(),
|
||||
},
|
||||
);
|
||||
const [isSubmitting, startTransition] = useTransition();
|
||||
const [error, setError] = useState<boolean>();
|
||||
|
||||
const onMemberRemoved = () => {
|
||||
startTransition(async () => {
|
||||
try {
|
||||
await removeMemberFromAccountAction({ accountId, userId });
|
||||
} catch {
|
||||
setError(true);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
execute({ accountId, userId });
|
||||
}}
|
||||
>
|
||||
<form action={onMemberRemoved}>
|
||||
<div className={'flex flex-col space-y-6'}>
|
||||
<p className={'text-muted-foreground text-sm'}>
|
||||
<Trans i18nKey={'common.modalConfirmationQuestion'} />
|
||||
<Trans i18nKey={'common:modalConfirmationQuestion'} />
|
||||
</p>
|
||||
|
||||
<If condition={hasErrored}>
|
||||
<If condition={error}>
|
||||
<RemoveMemberErrorAlert />
|
||||
</If>
|
||||
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>
|
||||
<Trans i18nKey={'common.cancel'} />
|
||||
<Trans i18nKey={'common:cancel'} />
|
||||
</AlertDialogCancel>
|
||||
|
||||
<Button
|
||||
data-test={'confirm-remove-member'}
|
||||
variant={'destructive'}
|
||||
disabled={isPending}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<Trans i18nKey={'teams.removeMemberSubmitLabel'} />
|
||||
<Trans i18nKey={'teams:removeMemberSubmitLabel'} />
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</div>
|
||||
@@ -106,11 +99,11 @@ function RemoveMemberErrorAlert() {
|
||||
return (
|
||||
<Alert variant={'destructive'}>
|
||||
<AlertTitle>
|
||||
<Trans i18nKey={'teams.removeMemberErrorHeading'} />
|
||||
<Trans i18nKey={'teams:removeMemberErrorHeading'} />
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
<Trans i18nKey={'teams.removeMemberErrorMessage'} />
|
||||
<Trans i18nKey={'teams:removeMemberErrorMessage'} />
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
|
||||
@@ -25,7 +25,7 @@ export function RoleBadge({ role }: { role: Role }) {
|
||||
return (
|
||||
<Badge className={className} variant={isCustom ? 'outline' : 'default'}>
|
||||
<span data-test={'member-role-badge'}>
|
||||
<Trans i18nKey={`common.roles.${role}.label`} defaults={role} />
|
||||
<Trans i18nKey={`common:roles.${role}.label`} defaults={role} />
|
||||
</span>
|
||||
</Badge>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useTransition } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
|
||||
import { VerifyOtpForm } from '@kit/otp/components';
|
||||
@@ -15,6 +16,7 @@ import {
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from '@kit/ui/alert-dialog';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { Form } from '@kit/ui/form';
|
||||
@@ -25,28 +27,30 @@ import { TransferOwnershipConfirmationSchema } from '../../schema/transfer-owner
|
||||
import { transferOwnershipAction } from '../../server/actions/team-members-server-actions';
|
||||
|
||||
export function TransferOwnershipDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
children,
|
||||
targetDisplayName,
|
||||
accountId,
|
||||
userId,
|
||||
}: {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
children: React.ReactNode;
|
||||
accountId: string;
|
||||
userId: string;
|
||||
targetDisplayName: string;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||
<AlertDialog open={open} onOpenChange={setOpen}>
|
||||
<AlertDialogTrigger asChild>{children}</AlertDialogTrigger>
|
||||
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
<Trans i18nKey="teams.transferOwnership" />
|
||||
<Trans i18nKey="team:transferOwnership" />
|
||||
</AlertDialogTitle>
|
||||
|
||||
<AlertDialogDescription>
|
||||
<Trans i18nKey="teams.transferOwnershipDescription" />
|
||||
<Trans i18nKey="team:transferOwnershipDescription" />
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
|
||||
@@ -54,7 +58,7 @@ export function TransferOwnershipDialog({
|
||||
accountId={accountId}
|
||||
userId={userId}
|
||||
targetDisplayName={targetDisplayName}
|
||||
onSuccess={() => onOpenChange(false)}
|
||||
onSuccess={() => setOpen(false)}
|
||||
/>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
@@ -72,15 +76,10 @@ function TransferOrganizationOwnershipForm({
|
||||
targetDisplayName: string;
|
||||
onSuccess: () => unknown;
|
||||
}) {
|
||||
const [pending, startTransition] = useTransition();
|
||||
const [error, setError] = useState<boolean>();
|
||||
const { data: user } = useUser();
|
||||
|
||||
const { execute, isPending, hasErrored } = useAction(
|
||||
transferOwnershipAction,
|
||||
{
|
||||
onSuccess: () => onSuccess(),
|
||||
},
|
||||
);
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(TransferOwnershipConfirmationSchema),
|
||||
defaultValues: {
|
||||
@@ -104,7 +103,7 @@ function TransferOrganizationOwnershipForm({
|
||||
}}
|
||||
CancelButton={
|
||||
<AlertDialogCancel>
|
||||
<Trans i18nKey={'common.cancel'} />
|
||||
<Trans i18nKey={'common:cancel'} />
|
||||
</AlertDialogCancel>
|
||||
}
|
||||
data-test="verify-otp-form"
|
||||
@@ -118,17 +117,25 @@ function TransferOrganizationOwnershipForm({
|
||||
<form
|
||||
className={'flex flex-col space-y-4 text-sm'}
|
||||
onSubmit={form.handleSubmit((data) => {
|
||||
execute(data);
|
||||
startTransition(async () => {
|
||||
try {
|
||||
await transferOwnershipAction(data);
|
||||
|
||||
onSuccess();
|
||||
} catch {
|
||||
setError(true);
|
||||
}
|
||||
});
|
||||
})}
|
||||
>
|
||||
<If condition={hasErrored}>
|
||||
<If condition={error}>
|
||||
<TransferOwnershipErrorAlert />
|
||||
</If>
|
||||
|
||||
<div className="border-destructive rounded-md border p-4">
|
||||
<p className="text-destructive text-sm">
|
||||
<Trans
|
||||
i18nKey={'teams.transferOwnershipDisclaimer'}
|
||||
i18nKey={'teams:transferOwnershipDisclaimer'}
|
||||
values={{
|
||||
member: targetDisplayName,
|
||||
}}
|
||||
@@ -141,26 +148,26 @@ function TransferOrganizationOwnershipForm({
|
||||
|
||||
<div>
|
||||
<p className={'text-muted-foreground'}>
|
||||
<Trans i18nKey={'common.modalConfirmationQuestion'} />
|
||||
<Trans i18nKey={'common:modalConfirmationQuestion'} />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>
|
||||
<Trans i18nKey={'common.cancel'} />
|
||||
<Trans i18nKey={'common:cancel'} />
|
||||
</AlertDialogCancel>
|
||||
|
||||
<Button
|
||||
type={'submit'}
|
||||
data-test={'confirm-transfer-ownership-button'}
|
||||
variant={'destructive'}
|
||||
disabled={isPending}
|
||||
disabled={pending}
|
||||
>
|
||||
<If
|
||||
condition={isPending}
|
||||
fallback={<Trans i18nKey={'teams.transferOwnership'} />}
|
||||
condition={pending}
|
||||
fallback={<Trans i18nKey={'teams:transferOwnership'} />}
|
||||
>
|
||||
<Trans i18nKey={'teams.transferringOwnership'} />
|
||||
<Trans i18nKey={'teams:transferringOwnership'} />
|
||||
</If>
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
@@ -173,11 +180,11 @@ function TransferOwnershipErrorAlert() {
|
||||
return (
|
||||
<Alert variant={'destructive'}>
|
||||
<AlertTitle>
|
||||
<Trans i18nKey={'teams.transferTeamErrorHeading'} />
|
||||
<Trans i18nKey={'teams:transferTeamErrorHeading'} />
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
<Trans i18nKey={'teams.transferTeamErrorMessage'} />
|
||||
<Trans i18nKey={'teams:transferTeamErrorMessage'} />
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
'use client';
|
||||
import { useState, useTransition } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||
import { AlertDialogCancel } from '@kit/ui/alert-dialog';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
@@ -14,6 +12,7 @@ import {
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@kit/ui/dialog';
|
||||
import {
|
||||
Form,
|
||||
@@ -35,30 +34,31 @@ import { RolesDataProvider } from './roles-data-provider';
|
||||
type Role = string;
|
||||
|
||||
export function UpdateMemberRoleDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
children,
|
||||
userId,
|
||||
teamAccountId,
|
||||
userRole,
|
||||
userRoleHierarchy,
|
||||
}: {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}: React.PropsWithChildren<{
|
||||
userId: string;
|
||||
teamAccountId: string;
|
||||
userRole: Role;
|
||||
userRoleHierarchy: number;
|
||||
}) {
|
||||
}>) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange} disablePointerDismissal>
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<Trans i18nKey={'teams.updateMemberRoleModalHeading'} />
|
||||
<Trans i18nKey={'teams:updateMemberRoleModalHeading'} />
|
||||
</DialogTitle>
|
||||
|
||||
<DialogDescription>
|
||||
<Trans i18nKey={'teams.updateMemberRoleModalDescription'} />
|
||||
<Trans i18nKey={'teams:updateMemberRoleModalDescription'} />
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -69,7 +69,7 @@ export function UpdateMemberRoleDialog({
|
||||
teamAccountId={teamAccountId}
|
||||
userRole={userRole}
|
||||
roles={data}
|
||||
onSuccess={() => onOpenChange(false)}
|
||||
onSuccess={() => setOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</RolesDataProvider>
|
||||
@@ -91,11 +91,25 @@ function UpdateMemberForm({
|
||||
roles: Role[];
|
||||
onSuccess: () => unknown;
|
||||
}>) {
|
||||
const t = useTranslations('teams');
|
||||
const [pending, startTransition] = useTransition();
|
||||
const [error, setError] = useState<boolean>();
|
||||
const { t } = useTranslation('teams');
|
||||
|
||||
const { execute, isPending, hasErrored } = useAction(updateMemberRoleAction, {
|
||||
onSuccess: () => onSuccess(),
|
||||
});
|
||||
const onSubmit = ({ role }: { role: Role }) => {
|
||||
startTransition(async () => {
|
||||
try {
|
||||
await updateMemberRoleAction({
|
||||
accountId: teamAccountId,
|
||||
userId,
|
||||
role,
|
||||
});
|
||||
|
||||
onSuccess();
|
||||
} catch {
|
||||
setError(true);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(
|
||||
@@ -120,16 +134,10 @@ function UpdateMemberForm({
|
||||
<Form {...form}>
|
||||
<form
|
||||
data-test={'update-member-role-form'}
|
||||
onSubmit={form.handleSubmit(({ role }) => {
|
||||
execute({
|
||||
accountId: teamAccountId,
|
||||
userId,
|
||||
role,
|
||||
});
|
||||
})}
|
||||
className={'flex w-full flex-col space-y-6'}
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className={'flex flex-col space-y-6'}
|
||||
>
|
||||
<If condition={hasErrored}>
|
||||
<If condition={error}>
|
||||
<UpdateRoleErrorAlert />
|
||||
</If>
|
||||
|
||||
@@ -142,15 +150,10 @@ function UpdateMemberForm({
|
||||
|
||||
<FormControl>
|
||||
<MembershipRoleSelector
|
||||
triggerClassName={'w-full'}
|
||||
roles={roles}
|
||||
currentUserRole={userRole}
|
||||
value={field.value}
|
||||
onChange={(newRole) => {
|
||||
if (newRole) {
|
||||
form.setValue('role', newRole);
|
||||
}
|
||||
}}
|
||||
onChange={(newRole) => form.setValue('role', newRole)}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
@@ -162,19 +165,9 @@ function UpdateMemberForm({
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="flex justify-end gap-x-2">
|
||||
<AlertDialogCancel>
|
||||
<Trans i18nKey={'common.cancel'} />
|
||||
</AlertDialogCancel>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
data-test={'confirm-update-member-role'}
|
||||
disabled={isPending}
|
||||
>
|
||||
<Trans i18nKey={'teams.updateRoleSubmitLabel'} />
|
||||
</Button>
|
||||
</div>
|
||||
<Button data-test={'confirm-update-member-role'} disabled={pending}>
|
||||
<Trans i18nKey={'teams:updateRoleSubmitLabel'} />
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
@@ -184,11 +177,11 @@ function UpdateRoleErrorAlert() {
|
||||
return (
|
||||
<Alert variant={'destructive'}>
|
||||
<AlertTitle>
|
||||
<Trans i18nKey={'teams.updateRoleErrorHeading'} />
|
||||
<Trans i18nKey={'teams:updateRoleErrorHeading'} />
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
<Trans i18nKey={'teams.updateRoleErrorMessage'} />
|
||||
<Trans i18nKey={'teams:updateRoleErrorMessage'} />
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import { useFormStatus } from 'react-dom';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ErrorBoundary } from '@kit/monitoring/components';
|
||||
import { VerifyOtpForm } from '@kit/otp/components';
|
||||
@@ -99,12 +100,12 @@ function DeleteTeamContainer(props: {
|
||||
<div className={'flex flex-col space-y-4'}>
|
||||
<div className={'flex flex-col space-y-1'}>
|
||||
<span className={'text-sm font-medium'}>
|
||||
<Trans i18nKey={'teams.deleteTeam'} />
|
||||
<Trans i18nKey={'teams:deleteTeam'} />
|
||||
</span>
|
||||
|
||||
<p className={'text-muted-foreground text-sm'}>
|
||||
<Trans
|
||||
i18nKey={'teams.deleteTeamDescription'}
|
||||
i18nKey={'teams:deleteTeamDescription'}
|
||||
values={{
|
||||
teamName: props.account.name,
|
||||
}}
|
||||
@@ -114,27 +115,25 @@ function DeleteTeamContainer(props: {
|
||||
|
||||
<div>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger
|
||||
render={
|
||||
<Button
|
||||
data-test={'delete-team-trigger'}
|
||||
type={'button'}
|
||||
variant={'destructive'}
|
||||
>
|
||||
<Trans i18nKey={'teams.deleteTeam'} />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
data-test={'delete-team-trigger'}
|
||||
type={'button'}
|
||||
variant={'destructive'}
|
||||
>
|
||||
<Trans i18nKey={'teams:deleteTeam'} />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
|
||||
<AlertDialogContent>
|
||||
<AlertDialogContent onEscapeKeyDown={(e) => e.preventDefault()}>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
<Trans i18nKey={'teams.deletingTeam'} />
|
||||
<Trans i18nKey={'teams:deletingTeam'} />
|
||||
</AlertDialogTitle>
|
||||
|
||||
<AlertDialogDescription>
|
||||
<Trans
|
||||
i18nKey={'teams.deletingTeamDescription'}
|
||||
i18nKey={'teams:deletingTeamDescription'}
|
||||
values={{
|
||||
teamName: props.account.name,
|
||||
}}
|
||||
@@ -162,8 +161,6 @@ function DeleteTeamConfirmationForm({
|
||||
}) {
|
||||
const { data: user } = useUser();
|
||||
|
||||
const { execute, isPending } = useAction(deleteTeamAccountAction);
|
||||
|
||||
const form = useForm({
|
||||
mode: 'onChange',
|
||||
reValidateMode: 'onChange',
|
||||
@@ -191,7 +188,7 @@ function DeleteTeamConfirmationForm({
|
||||
onSuccess={(otp) => form.setValue('otp', otp, { shouldValidate: true })}
|
||||
CancelButton={
|
||||
<AlertDialogCancel className={'m-0'}>
|
||||
<Trans i18nKey={'common.cancel'} />
|
||||
<Trans i18nKey={'common:cancel'} />
|
||||
</AlertDialogCancel>
|
||||
}
|
||||
/>
|
||||
@@ -204,10 +201,7 @@ function DeleteTeamConfirmationForm({
|
||||
<form
|
||||
data-test={'delete-team-form'}
|
||||
className={'flex flex-col space-y-4'}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
execute({ accountId: id, otp });
|
||||
}}
|
||||
action={deleteTeamAccountAction}
|
||||
>
|
||||
<div className={'flex flex-col space-y-2'}>
|
||||
<div
|
||||
@@ -217,7 +211,7 @@ function DeleteTeamConfirmationForm({
|
||||
>
|
||||
<div>
|
||||
<Trans
|
||||
i18nKey={'teams.deleteTeamDisclaimer'}
|
||||
i18nKey={'teams:deleteTeamDisclaimer'}
|
||||
values={{
|
||||
teamName: name,
|
||||
}}
|
||||
@@ -225,24 +219,20 @@ function DeleteTeamConfirmationForm({
|
||||
</div>
|
||||
|
||||
<div className={'text-sm'}>
|
||||
<Trans i18nKey={'common.modalConfirmationQuestion'} />
|
||||
<Trans i18nKey={'common:modalConfirmationQuestion'} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="hidden" value={id} name={'accountId'} />
|
||||
<input type="hidden" value={otp} name={'otp'} />
|
||||
</div>
|
||||
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>
|
||||
<Trans i18nKey={'common.cancel'} />
|
||||
<Trans i18nKey={'common:cancel'} />
|
||||
</AlertDialogCancel>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
data-test={'delete-team-form-confirm-button'}
|
||||
disabled={isPending}
|
||||
variant={'destructive'}
|
||||
>
|
||||
<Trans i18nKey={'teams.deleteTeam'} />
|
||||
</Button>
|
||||
<DeleteTeamSubmitButton />
|
||||
</AlertDialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
@@ -250,14 +240,26 @@ function DeleteTeamConfirmationForm({
|
||||
);
|
||||
}
|
||||
|
||||
function DeleteTeamSubmitButton() {
|
||||
const { pending } = useFormStatus();
|
||||
|
||||
return (
|
||||
<Button
|
||||
data-test={'delete-team-form-confirm-button'}
|
||||
disabled={pending}
|
||||
variant={'destructive'}
|
||||
>
|
||||
<Trans i18nKey={'teams:deleteTeam'} />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function LeaveTeamContainer(props: {
|
||||
account: {
|
||||
name: string;
|
||||
id: string;
|
||||
};
|
||||
}) {
|
||||
const { execute, isPending } = useAction(leaveTeamAccountAction);
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(
|
||||
z.object({
|
||||
@@ -276,7 +278,7 @@ function LeaveTeamContainer(props: {
|
||||
<div className={'flex flex-col space-y-4'}>
|
||||
<p className={'text-muted-foreground text-sm'}>
|
||||
<Trans
|
||||
i18nKey={'teams.leaveTeamDescription'}
|
||||
i18nKey={'teams:leaveTeamDescription'}
|
||||
values={{
|
||||
teamName: props.account.name,
|
||||
}}
|
||||
@@ -284,26 +286,26 @@ function LeaveTeamContainer(props: {
|
||||
</p>
|
||||
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger
|
||||
render={
|
||||
<AlertDialogTrigger asChild>
|
||||
<div>
|
||||
<Button
|
||||
data-test={'leave-team-button'}
|
||||
type={'button'}
|
||||
variant={'destructive'}
|
||||
>
|
||||
<Trans i18nKey={'teams.leaveTeam'} />
|
||||
<Trans i18nKey={'teams:leaveTeam'} />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</AlertDialogTrigger>
|
||||
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
<Trans i18nKey={'teams.leavingTeamModalHeading'} />
|
||||
<Trans i18nKey={'teams:leavingTeamModalHeading'} />
|
||||
</AlertDialogTitle>
|
||||
|
||||
<AlertDialogDescription>
|
||||
<Trans i18nKey={'teams.leavingTeamModalDescription'} />
|
||||
<Trans i18nKey={'teams:leavingTeamModalDescription'} />
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
|
||||
@@ -311,20 +313,21 @@ function LeaveTeamContainer(props: {
|
||||
<Form {...form}>
|
||||
<form
|
||||
className={'flex flex-col space-y-4'}
|
||||
onSubmit={form.handleSubmit((data) => {
|
||||
execute({
|
||||
accountId: props.account.id,
|
||||
confirmation: data.confirmation,
|
||||
});
|
||||
})}
|
||||
action={leaveTeamAccountAction}
|
||||
>
|
||||
<input
|
||||
type={'hidden'}
|
||||
value={props.account.id}
|
||||
name={'accountId'}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
name={'confirmation'}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans i18nKey={'teams.leaveTeamInputLabel'} />
|
||||
<Trans i18nKey={'teams:leaveTeamInputLabel'} />
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
@@ -341,7 +344,7 @@ function LeaveTeamContainer(props: {
|
||||
</FormControl>
|
||||
|
||||
<FormDescription>
|
||||
<Trans i18nKey={'teams.leaveTeamInputDescription'} />
|
||||
<Trans i18nKey={'teams:leaveTeamInputDescription'} />
|
||||
</FormDescription>
|
||||
|
||||
<FormMessage />
|
||||
@@ -352,17 +355,10 @@ function LeaveTeamContainer(props: {
|
||||
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>
|
||||
<Trans i18nKey={'common.cancel'} />
|
||||
<Trans i18nKey={'common:cancel'} />
|
||||
</AlertDialogCancel>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
data-test={'confirm-leave-organization-button'}
|
||||
disabled={isPending}
|
||||
variant={'destructive'}
|
||||
>
|
||||
<Trans i18nKey={'teams.leaveTeam'} />
|
||||
</Button>
|
||||
<LeaveTeamSubmitButton />
|
||||
</AlertDialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
@@ -373,22 +369,36 @@ function LeaveTeamContainer(props: {
|
||||
);
|
||||
}
|
||||
|
||||
function LeaveTeamSubmitButton() {
|
||||
const { pending } = useFormStatus();
|
||||
|
||||
return (
|
||||
<Button
|
||||
data-test={'confirm-leave-organization-button'}
|
||||
disabled={pending}
|
||||
variant={'destructive'}
|
||||
>
|
||||
<Trans i18nKey={'teams:leaveTeam'} />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function LeaveTeamErrorAlert() {
|
||||
return (
|
||||
<div className={'flex flex-col space-y-4'}>
|
||||
<Alert variant={'destructive'}>
|
||||
<AlertTitle>
|
||||
<Trans i18nKey={'teams.leaveTeamErrorHeading'} />
|
||||
<Trans i18nKey={'teams:leaveTeamErrorHeading'} />
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
<Trans i18nKey={'common.genericError'} />
|
||||
<Trans i18nKey={'common:genericError'} />
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>
|
||||
<Trans i18nKey={'common.cancel'} />
|
||||
<Trans i18nKey={'common:cancel'} />
|
||||
</AlertDialogCancel>
|
||||
</AlertDialogFooter>
|
||||
</div>
|
||||
@@ -400,17 +410,17 @@ function DeleteTeamErrorAlert() {
|
||||
<div className={'flex flex-col space-y-4'}>
|
||||
<Alert variant={'destructive'}>
|
||||
<AlertTitle>
|
||||
<Trans i18nKey={'teams.deleteTeamErrorHeading'} />
|
||||
<Trans i18nKey={'teams:deleteTeamErrorHeading'} />
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
<Trans i18nKey={'common.genericError'} />
|
||||
<Trans i18nKey={'common:genericError'} />
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>
|
||||
<Trans i18nKey={'common.cancel'} />
|
||||
<Trans i18nKey={'common:cancel'} />
|
||||
</AlertDialogCancel>
|
||||
</AlertDialogFooter>
|
||||
</div>
|
||||
@@ -422,11 +432,11 @@ function DangerZoneCard({ children }: React.PropsWithChildren) {
|
||||
<Card className={'border-destructive border'}>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
<Trans i18nKey={'teams.settings.dangerZone'} />
|
||||
<Trans i18nKey={'teams:settings.dangerZone'} />
|
||||
</CardTitle>
|
||||
|
||||
<CardDescription>
|
||||
<Trans i18nKey={'teams.settings.dangerZoneDescription'} />
|
||||
<Trans i18nKey={'teams:settings.dangerZoneDescription'} />
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
|
||||
@@ -35,11 +35,11 @@ export function TeamAccountSettingsContainer(props: {
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
<Trans i18nKey={'teams.settings.teamLogo'} />
|
||||
<Trans i18nKey={'teams:settings.teamLogo'} />
|
||||
</CardTitle>
|
||||
|
||||
<CardDescription>
|
||||
<Trans i18nKey={'teams.settings.teamLogoDescription'} />
|
||||
<Trans i18nKey={'teams:settings.teamLogoDescription'} />
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
@@ -51,11 +51,11 @@ export function TeamAccountSettingsContainer(props: {
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
<Trans i18nKey={'teams.settings.teamName'} />
|
||||
<Trans i18nKey={'teams:settings.teamName'} />
|
||||
</CardTitle>
|
||||
|
||||
<CardDescription>
|
||||
<Trans i18nKey={'teams.settings.teamNameDescription'} />
|
||||
<Trans i18nKey={'teams:settings.teamNameDescription'} />
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useCallback } from 'react';
|
||||
|
||||
import type { SupabaseClient } from '@supabase/supabase-js';
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useSupabase } from '@kit/supabase/hooks/use-supabase';
|
||||
import { ImageUploader } from '@kit/ui/image-uploader';
|
||||
@@ -21,7 +21,7 @@ export function UpdateTeamAccountImage(props: {
|
||||
};
|
||||
}) {
|
||||
const client = useSupabase();
|
||||
const t = useTranslations('teams');
|
||||
const { t } = useTranslation('teams');
|
||||
|
||||
const createToaster = useCallback(
|
||||
(promise: () => Promise<unknown>) => {
|
||||
@@ -89,11 +89,11 @@ export function UpdateTeamAccountImage(props: {
|
||||
>
|
||||
<div className={'flex flex-col space-y-1'}>
|
||||
<span className={'text-sm'}>
|
||||
<Trans i18nKey={'account.profilePictureHeading'} />
|
||||
<Trans i18nKey={'account:profilePictureHeading'} />
|
||||
</span>
|
||||
|
||||
<span className={'text-xs'}>
|
||||
<Trans i18nKey={'account.profilePictureSubheading'} />
|
||||
<Trans i18nKey={'account:profilePictureSubheading'} />
|
||||
</span>
|
||||
</div>
|
||||
</ImageUploader>
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import { useTransition } from 'react';
|
||||
|
||||
import { isRedirectError } from 'next/dist/client/components/redirect-error';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Building, Link } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Button } from '@kit/ui/button';
|
||||
import {
|
||||
@@ -37,7 +40,8 @@ export const UpdateTeamAccountNameForm = (props: {
|
||||
|
||||
path: string;
|
||||
}) => {
|
||||
const t = useTranslations('teams');
|
||||
const [pending, startTransition] = useTransition();
|
||||
const { t } = useTranslation('teams');
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(TeamNameFormSchema),
|
||||
@@ -47,21 +51,6 @@ export const UpdateTeamAccountNameForm = (props: {
|
||||
},
|
||||
});
|
||||
|
||||
const { execute, isPending } = useAction(updateTeamAccountName, {
|
||||
onSuccess: ({ data }) => {
|
||||
if (data?.success) {
|
||||
toast.success(t('updateTeamSuccessMessage'));
|
||||
} else if (data?.error) {
|
||||
toast.error(t(data.error));
|
||||
} else {
|
||||
toast.error(t('updateTeamErrorMessage'));
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(t('updateTeamErrorMessage'));
|
||||
},
|
||||
});
|
||||
|
||||
const nameValue = useWatch({ control: form.control, name: 'name' });
|
||||
const showSlugField = containsNonLatinCharacters(nameValue || '');
|
||||
|
||||
@@ -72,11 +61,41 @@ export const UpdateTeamAccountNameForm = (props: {
|
||||
data-test={'update-team-account-name-form'}
|
||||
className={'flex flex-col space-y-4'}
|
||||
onSubmit={form.handleSubmit((data) => {
|
||||
execute({
|
||||
slug: props.account.slug,
|
||||
name: data.name,
|
||||
newSlug: data.newSlug || undefined,
|
||||
path: props.path,
|
||||
startTransition(async () => {
|
||||
const toastId = toast.loading(t('updateTeamLoadingMessage'));
|
||||
|
||||
try {
|
||||
const result = await updateTeamAccountName({
|
||||
slug: props.account.slug,
|
||||
name: data.name,
|
||||
newSlug: data.newSlug || undefined,
|
||||
path: props.path,
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
toast.success(t('updateTeamSuccessMessage'), {
|
||||
id: toastId,
|
||||
});
|
||||
} else if (result.error) {
|
||||
toast.error(t(result.error), {
|
||||
id: toastId,
|
||||
});
|
||||
} else {
|
||||
toast.error(t('updateTeamErrorMessage'), {
|
||||
id: toastId,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (!isRedirectError(error)) {
|
||||
toast.error(t('updateTeamErrorMessage'), {
|
||||
id: toastId,
|
||||
});
|
||||
} else {
|
||||
toast.success(t('updateTeamSuccessMessage'), {
|
||||
id: toastId,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
})}
|
||||
>
|
||||
@@ -86,7 +105,7 @@ export const UpdateTeamAccountNameForm = (props: {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans i18nKey={'teams.teamNameLabel'} />
|
||||
<Trans i18nKey={'teams:teamNameLabel'} />
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
@@ -98,7 +117,7 @@ export const UpdateTeamAccountNameForm = (props: {
|
||||
<InputGroupInput
|
||||
data-test={'team-name-input'}
|
||||
required
|
||||
placeholder={t('teamNameInputLabel')}
|
||||
placeholder={t('teams:teamNameInputLabel')}
|
||||
{...field}
|
||||
/>
|
||||
</InputGroup>
|
||||
@@ -117,7 +136,7 @@ export const UpdateTeamAccountNameForm = (props: {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans i18nKey={'teams.teamSlugLabel'} />
|
||||
<Trans i18nKey={'teams:teamSlugLabel'} />
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
@@ -136,7 +155,7 @@ export const UpdateTeamAccountNameForm = (props: {
|
||||
</FormControl>
|
||||
|
||||
<FormDescription>
|
||||
<Trans i18nKey={'teams.teamSlugDescription'} />
|
||||
<Trans i18nKey={'teams:teamSlugDescription'} />
|
||||
</FormDescription>
|
||||
|
||||
<FormMessage />
|
||||
@@ -148,12 +167,11 @@ export const UpdateTeamAccountNameForm = (props: {
|
||||
|
||||
<div>
|
||||
<Button
|
||||
type="submit"
|
||||
className={'w-full md:w-auto'}
|
||||
data-test={'update-team-submit-button'}
|
||||
disabled={isPending}
|
||||
disabled={pending}
|
||||
>
|
||||
<Trans i18nKey={'teams.updateTeamSubmitLabel'} />
|
||||
<Trans i18nKey={'teams:updateTeamSubmitLabel'} />
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const AcceptInvitationSchema = z.object({
|
||||
inviteToken: z.string().uuid(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
/**
|
||||
* @name RESERVED_NAMES_ARRAY
|
||||
@@ -40,18 +40,20 @@ export function containsNonLatinCharacters(value: string): boolean {
|
||||
* @description Schema for validating URL-friendly slugs
|
||||
*/
|
||||
export const SlugSchema = z
|
||||
.string()
|
||||
.string({
|
||||
description: 'URL-friendly identifier for the team',
|
||||
})
|
||||
.min(2)
|
||||
.max(50)
|
||||
.regex(SLUG_REGEX, {
|
||||
message: 'teams.invalidSlugError',
|
||||
message: 'teams:invalidSlugError',
|
||||
})
|
||||
.refine(
|
||||
(slug) => {
|
||||
return !RESERVED_NAMES_ARRAY.includes(slug.toLowerCase());
|
||||
},
|
||||
{
|
||||
message: 'teams.reservedNameError',
|
||||
message: 'teams:reservedNameError',
|
||||
},
|
||||
);
|
||||
|
||||
@@ -60,7 +62,9 @@ export const SlugSchema = z
|
||||
* @description Schema for team name - allows non-Latin characters
|
||||
*/
|
||||
export const TeamNameSchema = z
|
||||
.string()
|
||||
.string({
|
||||
description: 'The name of the team account',
|
||||
})
|
||||
.min(2)
|
||||
.max(50)
|
||||
.refine(
|
||||
@@ -68,7 +72,7 @@ export const TeamNameSchema = z
|
||||
return !SPECIAL_CHARACTERS_REGEX.test(name);
|
||||
},
|
||||
{
|
||||
message: 'teams.specialCharactersError',
|
||||
message: 'teams:specialCharactersError',
|
||||
},
|
||||
)
|
||||
.refine(
|
||||
@@ -76,7 +80,7 @@ export const TeamNameSchema = z
|
||||
return !RESERVED_NAMES_ARRAY.includes(name.toLowerCase());
|
||||
},
|
||||
{
|
||||
message: 'teams.reservedNameError',
|
||||
message: 'teams:reservedNameError',
|
||||
},
|
||||
);
|
||||
|
||||
@@ -89,11 +93,10 @@ export const CreateTeamSchema = z
|
||||
.object({
|
||||
name: TeamNameSchema,
|
||||
// Transform empty strings to undefined before validation
|
||||
slug: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((val) => (val === '' ? undefined : val))
|
||||
.pipe(SlugSchema.optional()),
|
||||
slug: z.preprocess(
|
||||
(val) => (val === '' ? undefined : val),
|
||||
SlugSchema.optional(),
|
||||
),
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
@@ -104,7 +107,7 @@ export const CreateTeamSchema = z
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: 'teams.slugRequiredForNonLatinName',
|
||||
message: 'teams:slugRequiredForNonLatinName',
|
||||
path: ['slug'],
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const DeleteInvitationSchema = z.object({
|
||||
invitationId: z.number().int(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const DeleteTeamAccountSchema = z.object({
|
||||
accountId: z.string().uuid(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
const InviteSchema = z.object({
|
||||
email: z.string().email(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const LeaveTeamAccountSchema = z.object({
|
||||
accountId: z.string().uuid(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const RemoveMemberSchema = z.object({
|
||||
accountId: z.string().uuid(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const RenewInvitationSchema = z.object({
|
||||
invitationId: z.number().positive(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const TransferOwnershipConfirmationSchema = z.object({
|
||||
accountId: z.string().uuid(),
|
||||
@@ -6,6 +6,6 @@ export const TransferOwnershipConfirmationSchema = z.object({
|
||||
otp: z.string().min(6),
|
||||
});
|
||||
|
||||
export type TransferOwnershipConfirmationData = z.output<
|
||||
export type TransferOwnershipConfirmationData = z.infer<
|
||||
typeof TransferOwnershipConfirmationSchema
|
||||
>;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const UpdateInvitationSchema = z.object({
|
||||
invitationId: z.number(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const RoleSchema = z.object({
|
||||
role: z.string().min(1),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
SlugSchema,
|
||||
@@ -23,7 +23,7 @@ export const TeamNameFormSchema = z
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: 'teams.slugRequiredForNonLatinName',
|
||||
message: 'teams:slugRequiredForNonLatinName',
|
||||
path: ['newSlug'],
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
'use server';
|
||||
|
||||
import 'server-only';
|
||||
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
import { authActionClient } from '@kit/next/safe-action';
|
||||
import { enhanceAction } from '@kit/next/actions';
|
||||
import { getLogger } from '@kit/shared/logger';
|
||||
|
||||
import { CreateTeamSchema } from '../../schema/create-team.schema';
|
||||
import { createAccountCreationPolicyEvaluator } from '../policies';
|
||||
import { createCreateTeamAccountService } from '../services/create-team-account.service';
|
||||
|
||||
export const createTeamAccountAction = authActionClient
|
||||
.schema(CreateTeamSchema)
|
||||
.action(async ({ parsedInput: { name, slug }, ctx: { user } }) => {
|
||||
export const createTeamAccountAction = enhanceAction(
|
||||
async ({ name, slug }, user) => {
|
||||
const logger = await getLogger();
|
||||
const service = createCreateTeamAccountService();
|
||||
|
||||
@@ -60,7 +61,7 @@ export const createTeamAccountAction = authActionClient
|
||||
if (error === 'duplicate_slug') {
|
||||
return {
|
||||
error: true,
|
||||
message: 'teams.duplicateSlugError',
|
||||
message: 'teams:duplicateSlugError',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -69,4 +70,8 @@ export const createTeamAccountAction = authActionClient
|
||||
const accountHomePath = '/home/' + data.slug;
|
||||
|
||||
redirect(accountHomePath);
|
||||
});
|
||||
},
|
||||
{
|
||||
schema: CreateTeamSchema,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { redirect } from 'next/navigation';
|
||||
|
||||
import type { SupabaseClient } from '@supabase/supabase-js';
|
||||
|
||||
import { authActionClient } from '@kit/next/safe-action';
|
||||
import { enhanceAction } from '@kit/next/actions';
|
||||
import { createOtpApi } from '@kit/otp';
|
||||
import { getLogger } from '@kit/shared/logger';
|
||||
import type { Database } from '@kit/supabase/database';
|
||||
@@ -16,11 +16,14 @@ import { createDeleteTeamAccountService } from '../services/delete-team-account.
|
||||
const enableTeamAccountDeletion =
|
||||
process.env.NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_DELETION === 'true';
|
||||
|
||||
export const deleteTeamAccountAction = authActionClient
|
||||
.schema(DeleteTeamAccountSchema)
|
||||
.action(async ({ parsedInput: params, ctx: { user } }) => {
|
||||
export const deleteTeamAccountAction = enhanceAction(
|
||||
async (formData: FormData, user) => {
|
||||
const logger = await getLogger();
|
||||
|
||||
const params = DeleteTeamAccountSchema.parse(
|
||||
Object.fromEntries(formData.entries()),
|
||||
);
|
||||
|
||||
const otpService = createOtpApi(getSupabaseServerClient());
|
||||
|
||||
const otpResult = await otpService.verifyToken({
|
||||
@@ -54,8 +57,12 @@ export const deleteTeamAccountAction = authActionClient
|
||||
|
||||
logger.info(ctx, `Team account request successfully sent`);
|
||||
|
||||
redirect('/home');
|
||||
});
|
||||
return redirect('/home');
|
||||
},
|
||||
{
|
||||
auth: true,
|
||||
},
|
||||
);
|
||||
|
||||
async function deleteTeamAccount(params: {
|
||||
accountId: string;
|
||||
|
||||
@@ -3,15 +3,17 @@
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
import { authActionClient } from '@kit/next/safe-action';
|
||||
import { enhanceAction } from '@kit/next/actions';
|
||||
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
|
||||
|
||||
import { LeaveTeamAccountSchema } from '../../schema/leave-team-account.schema';
|
||||
import { createLeaveTeamAccountService } from '../services/leave-team-account.service';
|
||||
|
||||
export const leaveTeamAccountAction = authActionClient
|
||||
.schema(LeaveTeamAccountSchema)
|
||||
.action(async ({ parsedInput: params, ctx: { user } }) => {
|
||||
export const leaveTeamAccountAction = enhanceAction(
|
||||
async (formData: FormData, user) => {
|
||||
const body = Object.fromEntries(formData.entries());
|
||||
const params = LeaveTeamAccountSchema.parse(body);
|
||||
|
||||
const service = createLeaveTeamAccountService(
|
||||
getSupabaseServerAdminClient(),
|
||||
);
|
||||
@@ -23,5 +25,7 @@ export const leaveTeamAccountAction = authActionClient
|
||||
|
||||
revalidatePath('/home/[account]', 'layout');
|
||||
|
||||
redirect('/home');
|
||||
});
|
||||
return redirect('/home');
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
@@ -2,15 +2,14 @@
|
||||
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
import { authActionClient } from '@kit/next/safe-action';
|
||||
import { enhanceAction } from '@kit/next/actions';
|
||||
import { getLogger } from '@kit/shared/logger';
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
|
||||
import { UpdateTeamNameSchema } from '../../schema/update-team-name.schema';
|
||||
|
||||
export const updateTeamAccountName = authActionClient
|
||||
.schema(UpdateTeamNameSchema)
|
||||
.action(async ({ parsedInput: params }) => {
|
||||
export const updateTeamAccountName = enhanceAction(
|
||||
async (params) => {
|
||||
const client = getSupabaseServerClient();
|
||||
const logger = await getLogger();
|
||||
const { name, path, slug, newSlug } = params;
|
||||
@@ -41,7 +40,7 @@ export const updateTeamAccountName = authActionClient
|
||||
if (error.code === '23505') {
|
||||
return {
|
||||
success: false,
|
||||
error: 'teams.duplicateSlugError',
|
||||
error: 'teams:duplicateSlugError',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -61,4 +60,8 @@ export const updateTeamAccountName = authActionClient
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
});
|
||||
},
|
||||
{
|
||||
schema: UpdateTeamNameSchema,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { authActionClient } from '@kit/next/safe-action';
|
||||
import { enhanceAction } from '@kit/next/actions';
|
||||
import { getLogger } from '@kit/shared/logger';
|
||||
import { Database } from '@kit/supabase/database';
|
||||
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
|
||||
@@ -26,15 +26,8 @@ import { createAccountPerSeatBillingService } from '../services/account-per-seat
|
||||
* @name createInvitationsAction
|
||||
* @description Creates invitations for inviting members.
|
||||
*/
|
||||
export const createInvitationsAction = authActionClient
|
||||
.schema(
|
||||
InviteMembersSchema.and(
|
||||
z.object({
|
||||
accountSlug: z.string().min(1),
|
||||
}),
|
||||
),
|
||||
)
|
||||
.action(async ({ parsedInput: params, ctx: { user } }) => {
|
||||
export const createInvitationsAction = enhanceAction(
|
||||
async (params, user) => {
|
||||
const logger = await getLogger();
|
||||
|
||||
logger.info(
|
||||
@@ -123,15 +116,22 @@ export const createInvitationsAction = authActionClient
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
});
|
||||
},
|
||||
{
|
||||
schema: InviteMembersSchema.and(
|
||||
z.object({
|
||||
accountSlug: z.string().min(1),
|
||||
}),
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @name deleteInvitationAction
|
||||
* @description Deletes an invitation specified by the invitation ID.
|
||||
*/
|
||||
export const deleteInvitationAction = authActionClient
|
||||
.schema(DeleteInvitationSchema)
|
||||
.action(async ({ parsedInput: data }) => {
|
||||
export const deleteInvitationAction = enhanceAction(
|
||||
async (data) => {
|
||||
const client = getSupabaseServerClient();
|
||||
const service = createAccountInvitationsService(client);
|
||||
|
||||
@@ -143,15 +143,18 @@ export const deleteInvitationAction = authActionClient
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
});
|
||||
},
|
||||
{
|
||||
schema: DeleteInvitationSchema,
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @name updateInvitationAction
|
||||
* @description Updates an invitation.
|
||||
*/
|
||||
export const updateInvitationAction = authActionClient
|
||||
.schema(UpdateInvitationSchema)
|
||||
.action(async ({ parsedInput: invitation }) => {
|
||||
export const updateInvitationAction = enhanceAction(
|
||||
async (invitation) => {
|
||||
const client = getSupabaseServerClient();
|
||||
const service = createAccountInvitationsService(client);
|
||||
|
||||
@@ -162,18 +165,23 @@ export const updateInvitationAction = authActionClient
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
});
|
||||
},
|
||||
{
|
||||
schema: UpdateInvitationSchema,
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @name acceptInvitationAction
|
||||
* @description Accepts an invitation to join a team.
|
||||
*/
|
||||
export const acceptInvitationAction = authActionClient
|
||||
.schema(AcceptInvitationSchema)
|
||||
.action(async ({ parsedInput: data, ctx: { user } }) => {
|
||||
export const acceptInvitationAction = enhanceAction(
|
||||
async (data: FormData, user) => {
|
||||
const client = getSupabaseServerClient();
|
||||
|
||||
const { inviteToken, nextPath } = data;
|
||||
const { inviteToken, nextPath } = AcceptInvitationSchema.parse(
|
||||
Object.fromEntries(data),
|
||||
);
|
||||
|
||||
// create the services
|
||||
const perSeatBillingService = createAccountPerSeatBillingService(client);
|
||||
@@ -197,17 +205,19 @@ export const acceptInvitationAction = authActionClient
|
||||
// Increase the seats for the account
|
||||
await perSeatBillingService.increaseSeats(accountId);
|
||||
|
||||
redirect(nextPath);
|
||||
});
|
||||
return redirect(nextPath);
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
/**
|
||||
* @name renewInvitationAction
|
||||
* @description Renews an invitation.
|
||||
*/
|
||||
export const renewInvitationAction = authActionClient
|
||||
.schema(RenewInvitationSchema)
|
||||
.action(async ({ parsedInput: { invitationId } }) => {
|
||||
export const renewInvitationAction = enhanceAction(
|
||||
async (params) => {
|
||||
const client = getSupabaseServerClient();
|
||||
const { invitationId } = RenewInvitationSchema.parse(params);
|
||||
|
||||
const service = createAccountInvitationsService(client);
|
||||
|
||||
@@ -219,7 +229,11 @@ export const renewInvitationAction = authActionClient
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
});
|
||||
},
|
||||
{
|
||||
schema: RenewInvitationSchema,
|
||||
},
|
||||
);
|
||||
|
||||
function revalidateMemberPage() {
|
||||
revalidatePath('/home/[account]/members', 'page');
|
||||
@@ -233,7 +247,7 @@ function revalidateMemberPage() {
|
||||
* @param accountId - The account ID (already fetched to avoid duplicate queries).
|
||||
*/
|
||||
async function evaluateInvitationsPolicies(
|
||||
params: z.output<typeof InviteMembersSchema> & { accountSlug: string },
|
||||
params: z.infer<typeof InviteMembersSchema> & { accountSlug: string },
|
||||
user: JWTUserData,
|
||||
accountId: string,
|
||||
) {
|
||||
@@ -268,7 +282,7 @@ async function evaluateInvitationsPolicies(
|
||||
async function checkInvitationPermissions(
|
||||
accountId: string,
|
||||
userId: string,
|
||||
invitations: z.output<typeof InviteMembersSchema>['invitations'],
|
||||
invitations: z.infer<typeof InviteMembersSchema>['invitations'],
|
||||
): Promise<{
|
||||
allowed: boolean;
|
||||
reason?: string;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { revalidatePath } from 'next/cache';
|
||||
|
||||
import { authActionClient } from '@kit/next/safe-action';
|
||||
import { enhanceAction } from '@kit/next/actions';
|
||||
import { createOtpApi } from '@kit/otp';
|
||||
import { getLogger } from '@kit/shared/logger';
|
||||
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
|
||||
@@ -17,9 +17,8 @@ import { createAccountMembersService } from '../services/account-members.service
|
||||
* @name removeMemberFromAccountAction
|
||||
* @description Removes a member from an account.
|
||||
*/
|
||||
export const removeMemberFromAccountAction = authActionClient
|
||||
.schema(RemoveMemberSchema)
|
||||
.action(async ({ parsedInput: { accountId, userId } }) => {
|
||||
export const removeMemberFromAccountAction = enhanceAction(
|
||||
async ({ accountId, userId }) => {
|
||||
const client = getSupabaseServerClient();
|
||||
const service = createAccountMembersService(client);
|
||||
|
||||
@@ -32,15 +31,18 @@ export const removeMemberFromAccountAction = authActionClient
|
||||
revalidatePath('/home/[account]', 'layout');
|
||||
|
||||
return { success: true };
|
||||
});
|
||||
},
|
||||
{
|
||||
schema: RemoveMemberSchema,
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @name updateMemberRoleAction
|
||||
* @description Updates the role of a member in an account.
|
||||
*/
|
||||
export const updateMemberRoleAction = authActionClient
|
||||
.schema(UpdateMemberRoleSchema)
|
||||
.action(async ({ parsedInput: data }) => {
|
||||
export const updateMemberRoleAction = enhanceAction(
|
||||
async (data) => {
|
||||
const client = getSupabaseServerClient();
|
||||
const service = createAccountMembersService(client);
|
||||
const adminClient = getSupabaseServerAdminClient();
|
||||
@@ -52,16 +54,19 @@ export const updateMemberRoleAction = authActionClient
|
||||
revalidatePath('/home/[account]', 'layout');
|
||||
|
||||
return { success: true };
|
||||
});
|
||||
},
|
||||
{
|
||||
schema: UpdateMemberRoleSchema,
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @name transferOwnershipAction
|
||||
* @description Transfers the ownership of an account to another member.
|
||||
* Requires OTP verification for security.
|
||||
*/
|
||||
export const transferOwnershipAction = authActionClient
|
||||
.schema(TransferOwnershipConfirmationSchema)
|
||||
.action(async ({ parsedInput: data, ctx: { user } }) => {
|
||||
export const transferOwnershipAction = enhanceAction(
|
||||
async (data, user) => {
|
||||
const client = getSupabaseServerClient();
|
||||
const logger = await getLogger();
|
||||
|
||||
@@ -132,4 +137,8 @@ export const transferOwnershipAction = authActionClient
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
});
|
||||
},
|
||||
{
|
||||
schema: TransferOwnershipConfirmationSchema,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { SupabaseClient } from '@supabase/supabase-js';
|
||||
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { Database } from '@kit/supabase/database';
|
||||
import { JWTUserData } from '@kit/supabase/types';
|
||||
@@ -29,7 +29,7 @@ class InvitationContextBuilder {
|
||||
* Build policy context for invitation evaluation with optimized parallel loading
|
||||
*/
|
||||
async buildContext(
|
||||
params: z.output<typeof InviteMembersSchema> & { accountSlug: string },
|
||||
params: z.infer<typeof InviteMembersSchema> & { accountSlug: string },
|
||||
user: JWTUserData,
|
||||
): Promise<FeaturePolicyInvitationContext> {
|
||||
// Fetch all data in parallel for optimal performance
|
||||
@@ -43,7 +43,7 @@ class InvitationContextBuilder {
|
||||
* (avoids duplicate account lookup)
|
||||
*/
|
||||
async buildContextWithAccountId(
|
||||
params: z.output<typeof InviteMembersSchema> & { accountSlug: string },
|
||||
params: z.infer<typeof InviteMembersSchema> & { accountSlug: string },
|
||||
user: JWTUserData,
|
||||
accountId: string,
|
||||
): Promise<FeaturePolicyInvitationContext> {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user