Unify workspace dropdowns; Update layouts (#458)
Unified Account and Workspace drop-downs; Layout updates, now header lives within the PageBody component; Sidebars now use floating variant
This commit is contained in:
committed by
GitHub
parent
ca585e09be
commit
4bc8448a1d
@@ -44,10 +44,11 @@
|
||||
"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,14 +1,5 @@
|
||||
'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,
|
||||
@@ -16,24 +7,9 @@ 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 {
|
||||
CreateTeamSchema,
|
||||
NON_LATIN_REGEX,
|
||||
} from '../schema/create-team.schema';
|
||||
import { createTeamAccountAction } from '../server/actions/create-team-account-server-actions';
|
||||
import { CreateTeamAccountForm } from './create-team-account-form';
|
||||
|
||||
export function CreateTeamAccountDialog(
|
||||
props: React.PropsWithChildren<{
|
||||
@@ -42,171 +18,24 @@ export function CreateTeamAccountDialog(
|
||||
}>,
|
||||
) {
|
||||
return (
|
||||
<Dialog open={props.isOpen} onOpenChange={props.setIsOpen}>
|
||||
<DialogContent
|
||||
onEscapeKeyDown={(e) => e.preventDefault()}
|
||||
onInteractOutside={(e) => e.preventDefault()}
|
||||
>
|
||||
<Dialog
|
||||
open={props.isOpen}
|
||||
onOpenChange={props.setIsOpen}
|
||||
disablePointerDismissal
|
||||
>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<Trans i18nKey={'teams:createTeamModalHeading'} />
|
||||
<Trans i18nKey={'teams.createTeamModalHeading'} />
|
||||
</DialogTitle>
|
||||
|
||||
<DialogDescription>
|
||||
<Trans i18nKey={'teams:createTeamModalDescription'} />
|
||||
<Trans i18nKey={'teams.createTeamModalDescription'} />
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<CreateOrganizationAccountForm onClose={() => props.setIsOpen(false)} />
|
||||
<CreateTeamAccountForm onCancel={() => 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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
'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,4 +5,5 @@ 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,12 +1,16 @@
|
||||
'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: {
|
||||
@@ -28,11 +32,13 @@ 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,
|
||||
}}
|
||||
@@ -53,7 +59,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,
|
||||
}}
|
||||
@@ -64,20 +70,24 @@ export function AcceptInvitationContainer(props: {
|
||||
<form
|
||||
data-test={'join-team-form'}
|
||||
className={'w-full'}
|
||||
action={acceptInvitationAction}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
|
||||
execute({
|
||||
inviteToken: props.inviteToken,
|
||||
nextPath: props.paths.nextPath,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<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}
|
||||
/>
|
||||
<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>
|
||||
</form>
|
||||
|
||||
<Separator />
|
||||
@@ -85,7 +95,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 { useTranslation } from 'react-i18next';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
import { Database } from '@kit/supabase/database';
|
||||
import { Badge } from '@kit/ui/badge';
|
||||
@@ -43,7 +43,7 @@ export function AccountInvitationsTable({
|
||||
invitations,
|
||||
permissions,
|
||||
}: AccountInvitationsTableProps) {
|
||||
const { t } = useTranslation('teams');
|
||||
const t = useTranslations('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 } = useTranslation('teams');
|
||||
const t = useTranslations('teams');
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
@@ -96,7 +96,7 @@ function useGetColumns(permissions: {
|
||||
return (
|
||||
<span
|
||||
data-test={'invitation-email'}
|
||||
className={'flex items-center space-x-4 text-left'}
|
||||
className={'flex items-center gap-x-2 text-left'}
|
||||
>
|
||||
<span>
|
||||
<ProfileAvatar text={email} />
|
||||
@@ -172,19 +172,21 @@ function ActionsDropdown({
|
||||
return (
|
||||
<>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant={'ghost'} size={'icon'}>
|
||||
<Ellipsis className={'h-5 w-5'} />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<Button variant={'ghost'} size={'icon'}>
|
||||
<Ellipsis className={'h-5 w-5'} />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuContent className="min-w-52">
|
||||
<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)}>
|
||||
@@ -192,7 +194,7 @@ function ActionsDropdown({
|
||||
data-test={'renew-invitation-trigger'}
|
||||
onClick={() => setIsRenewingInvite(true)}
|
||||
>
|
||||
<Trans i18nKey={'teams:renewInvitation'} />
|
||||
<Trans i18nKey={'teams.renewInvitation'} />
|
||||
</DropdownMenuItem>
|
||||
</If>
|
||||
</If>
|
||||
@@ -202,7 +204,7 @@ function ActionsDropdown({
|
||||
data-test={'remove-invitation-trigger'}
|
||||
onClick={() => setIsDeletingInvite(true)}
|
||||
>
|
||||
<Trans i18nKey={'teams:removeInvitation'} />
|
||||
<Trans i18nKey={'teams.removeInvitation'} />
|
||||
</DropdownMenuItem>
|
||||
</If>
|
||||
</DropdownMenuContent>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { useState, useTransition } from 'react';
|
||||
'use client';
|
||||
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||
import {
|
||||
@@ -30,11 +32,11 @@ export function DeleteInvitationDialog({
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
<Trans i18nKey="team:deleteInvitation" />
|
||||
<Trans i18nKey="teams.deleteInvitation" />
|
||||
</AlertDialogTitle>
|
||||
|
||||
<AlertDialogDescription>
|
||||
<Trans i18nKey="team:deleteInvitationDialogDescription" />
|
||||
<Trans i18nKey="teams.deleteInvitationDialogDescription" />
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
|
||||
@@ -54,43 +56,34 @@ function DeleteInvitationForm({
|
||||
invitationId: number;
|
||||
setIsOpen: (isOpen: boolean) => void;
|
||||
}) {
|
||||
const [isSubmitting, startTransition] = useTransition();
|
||||
const [error, setError] = useState<boolean>();
|
||||
|
||||
const onInvitationRemoved = () => {
|
||||
startTransition(async () => {
|
||||
try {
|
||||
await deleteInvitationAction({ invitationId });
|
||||
|
||||
setIsOpen(false);
|
||||
} catch {
|
||||
setError(true);
|
||||
}
|
||||
});
|
||||
};
|
||||
const { execute, isPending, hasErrored } = useAction(deleteInvitationAction, {
|
||||
onSuccess: () => setIsOpen(false),
|
||||
});
|
||||
|
||||
return (
|
||||
<form data-test={'delete-invitation-form'} action={onInvitationRemoved}>
|
||||
<form
|
||||
data-test={'delete-invitation-form'}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
execute({ invitationId });
|
||||
}}
|
||||
>
|
||||
<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={error}>
|
||||
<If condition={hasErrored}>
|
||||
<RemoveInvitationErrorAlert />
|
||||
</If>
|
||||
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>
|
||||
<Trans i18nKey={'common:cancel'} />
|
||||
<Trans i18nKey={'common.cancel'} />
|
||||
</AlertDialogCancel>
|
||||
|
||||
<Button
|
||||
type={'submit'}
|
||||
variant={'destructive'}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<Trans i18nKey={'teams:deleteInvitation'} />
|
||||
<Button type={'submit'} variant={'destructive'} disabled={isPending}>
|
||||
<Trans i18nKey={'teams.deleteInvitation'} />
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</div>
|
||||
@@ -102,11 +95,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,4 +1,6 @@
|
||||
import { useState, useTransition } from 'react';
|
||||
'use client';
|
||||
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||
import {
|
||||
@@ -32,12 +34,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>
|
||||
@@ -59,42 +61,33 @@ function RenewInvitationForm({
|
||||
invitationId: number;
|
||||
setIsOpen: (isOpen: boolean) => void;
|
||||
}) {
|
||||
const [isSubmitting, startTransition] = useTransition();
|
||||
const [error, setError] = useState<boolean>();
|
||||
|
||||
const inInvitationRenewed = () => {
|
||||
startTransition(async () => {
|
||||
try {
|
||||
await renewInvitationAction({ invitationId });
|
||||
|
||||
setIsOpen(false);
|
||||
} catch {
|
||||
setError(true);
|
||||
}
|
||||
});
|
||||
};
|
||||
const { execute, isPending, hasErrored } = useAction(renewInvitationAction, {
|
||||
onSuccess: () => setIsOpen(false),
|
||||
});
|
||||
|
||||
return (
|
||||
<form action={inInvitationRenewed}>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
execute({ invitationId });
|
||||
}}
|
||||
>
|
||||
<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={error}>
|
||||
<If condition={hasErrored}>
|
||||
<RenewInvitationErrorAlert />
|
||||
</If>
|
||||
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>
|
||||
<Trans i18nKey={'common:cancel'} />
|
||||
<Trans i18nKey={'common.cancel'} />
|
||||
</AlertDialogCancel>
|
||||
|
||||
<Button
|
||||
data-test={'confirm-renew-invitation'}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<Trans i18nKey={'teams:renewInvitation'} />
|
||||
<Button data-test={'confirm-renew-invitation'} disabled={isPending}>
|
||||
<Trans i18nKey={'teams.renewInvitation'} />
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</div>
|
||||
@@ -106,11 +99,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,8 +1,9 @@
|
||||
import { useState, useTransition } from 'react';
|
||||
'use client';
|
||||
|
||||
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';
|
||||
@@ -50,11 +51,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>
|
||||
|
||||
@@ -80,24 +81,11 @@ function UpdateInvitationForm({
|
||||
userRoleHierarchy: number;
|
||||
setIsOpen: (isOpen: boolean) => void;
|
||||
}>) {
|
||||
const { t } = useTranslation('teams');
|
||||
const [pending, startTransition] = useTransition();
|
||||
const [error, setError] = useState<boolean>();
|
||||
const t = useTranslations('teams');
|
||||
|
||||
const onSubmit = ({ role }: { role: Role }) => {
|
||||
startTransition(async () => {
|
||||
try {
|
||||
await updateInvitationAction({
|
||||
invitationId,
|
||||
role,
|
||||
});
|
||||
|
||||
setIsOpen(false);
|
||||
} catch {
|
||||
setError(true);
|
||||
}
|
||||
});
|
||||
};
|
||||
const { execute, isPending, hasErrored } = useAction(updateInvitationAction, {
|
||||
onSuccess: () => setIsOpen(false),
|
||||
});
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(
|
||||
@@ -122,10 +110,12 @@ function UpdateInvitationForm({
|
||||
<Form {...form}>
|
||||
<form
|
||||
data-test={'update-invitation-form'}
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
onSubmit={form.handleSubmit(({ role }) => {
|
||||
execute({ invitationId, role });
|
||||
})}
|
||||
className={'flex flex-col space-y-6'}
|
||||
>
|
||||
<If condition={error}>
|
||||
<If condition={hasErrored}>
|
||||
<UpdateRoleErrorAlert />
|
||||
</If>
|
||||
|
||||
@@ -135,7 +125,7 @@ function UpdateInvitationForm({
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans i18nKey={'teams:roleLabel'} />
|
||||
<Trans i18nKey={'teams.roleLabel'} />
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
@@ -145,16 +135,18 @@ function UpdateInvitationForm({
|
||||
roles={roles}
|
||||
currentUserRole={userRole}
|
||||
value={field.value}
|
||||
onChange={(newRole) =>
|
||||
form.setValue(field.name, newRole)
|
||||
}
|
||||
onChange={(newRole) => {
|
||||
if (newRole) {
|
||||
form.setValue(field.name, newRole);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</RolesDataProvider>
|
||||
</FormControl>
|
||||
|
||||
<FormDescription>
|
||||
<Trans i18nKey={'teams:updateRoleDescription'} />
|
||||
<Trans i18nKey={'teams.updateRoleDescription'} />
|
||||
</FormDescription>
|
||||
|
||||
<FormMessage />
|
||||
@@ -163,8 +155,8 @@ function UpdateInvitationForm({
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button type={'submit'} disabled={pending}>
|
||||
<Trans i18nKey={'teams:updateRoleSubmitLabel'} />
|
||||
<Button type={'submit'} disabled={isPending}>
|
||||
<Trans i18nKey={'teams.updateRoleSubmitLabel'} />
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
@@ -175,11 +167,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 { useTranslation } from 'react-i18next';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
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 } = useTranslation('teams');
|
||||
const t = useTranslations('teams');
|
||||
|
||||
const permissions = {
|
||||
canUpdateRole: (targetRole: number) => {
|
||||
@@ -123,7 +123,7 @@ function useGetColumns(
|
||||
currentRoleHierarchy: number;
|
||||
},
|
||||
): ColumnDef<Members[0]>[] {
|
||||
const { t } = useTranslation('teams');
|
||||
const t = useTranslations('teams');
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
@@ -136,7 +136,7 @@ function useGetColumns(
|
||||
const isSelf = member.user_id === params.currentUserId;
|
||||
|
||||
return (
|
||||
<span className={'flex items-center space-x-4 text-left'}>
|
||||
<span className={'flex items-center gap-x-2 text-left'}>
|
||||
<span>
|
||||
<ProfileAvatar
|
||||
displayName={displayName}
|
||||
@@ -144,11 +144,13 @@ function useGetColumns(
|
||||
/>
|
||||
</span>
|
||||
|
||||
<span>{displayName}</span>
|
||||
<span className={'flex items-center gap-x-2'}>
|
||||
<span>{displayName}</span>
|
||||
|
||||
<If condition={isSelf}>
|
||||
<Badge variant={'outline'}>{t('youLabel')}</Badge>
|
||||
</If>
|
||||
<If condition={isSelf}>
|
||||
<Badge variant={'secondary'}>{t('youLabel')}</Badge>
|
||||
</If>
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
},
|
||||
@@ -171,13 +173,7 @@ function useGetColumns(
|
||||
<RoleBadge role={role} />
|
||||
|
||||
<If condition={isPrimaryOwner}>
|
||||
<span
|
||||
className={
|
||||
'rounded-md bg-yellow-400 px-2.5 py-1 text-xs font-medium dark:text-black'
|
||||
}
|
||||
>
|
||||
{t('primaryOwnerLabel')}
|
||||
</span>
|
||||
<Badge variant={'warning'}>{t('primaryOwnerLabel')}</Badge>
|
||||
</If>
|
||||
</span>
|
||||
);
|
||||
@@ -223,6 +219,10 @@ 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,50 +246,66 @@ function ActionsDropdown({
|
||||
return (
|
||||
<>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant={'ghost'} size={'icon'}>
|
||||
<Ellipsis className={'h-5 w-5'} />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<Button variant={'ghost'} size={'icon'}>
|
||||
<Ellipsis className={'h-5 w-5'} />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuContent className={'min-w-52'}>
|
||||
<If condition={canUpdateRole}>
|
||||
<UpdateMemberRoleDialog
|
||||
userId={member.user_id}
|
||||
userRole={member.role}
|
||||
teamAccountId={currentTeamAccountId}
|
||||
userRoleHierarchy={currentRoleHierarchy}
|
||||
>
|
||||
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
|
||||
<Trans i18nKey={'teams:updateRole'} />
|
||||
</DropdownMenuItem>
|
||||
</UpdateMemberRoleDialog>
|
||||
<DropdownMenuItem onClick={() => setActiveDialog('updateRole')}>
|
||||
<Trans i18nKey={'teams.updateRole'} />
|
||||
</DropdownMenuItem>
|
||||
</If>
|
||||
|
||||
<If condition={permissions.canTransferOwnership}>
|
||||
<TransferOwnershipDialog
|
||||
targetDisplayName={member.name ?? member.email}
|
||||
accountId={member.account_id}
|
||||
userId={member.user_id}
|
||||
<DropdownMenuItem
|
||||
onClick={() => setActiveDialog('transferOwnership')}
|
||||
>
|
||||
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
|
||||
<Trans i18nKey={'teams:transferOwnership'} />
|
||||
</DropdownMenuItem>
|
||||
</TransferOwnershipDialog>
|
||||
<Trans i18nKey={'teams.transferOwnership'} />
|
||||
</DropdownMenuItem>
|
||||
</If>
|
||||
|
||||
<If condition={canRemoveFromAccount}>
|
||||
<RemoveMemberDialog
|
||||
teamAccountId={currentTeamAccountId}
|
||||
userId={member.user_id}
|
||||
>
|
||||
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
|
||||
<Trans i18nKey={'teams:removeMember'} />
|
||||
</DropdownMenuItem>
|
||||
</RemoveMemberDialog>
|
||||
<DropdownMenuItem onClick={() => setActiveDialog('removeMember')}>
|
||||
<Trans i18nKey={'teams.removeMember'} />
|
||||
</DropdownMenuItem>
|
||||
</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,12 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useTransition } from 'react';
|
||||
import { useState } 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';
|
||||
@@ -64,9 +65,24 @@ export function InviteMembersDialogContainer({
|
||||
accountSlug: string;
|
||||
userRoleHierarchy: number;
|
||||
}>) {
|
||||
const [pending, startTransition] = useTransition();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { t } = useTranslation('teams');
|
||||
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);
|
||||
},
|
||||
});
|
||||
|
||||
// Evaluate policies when dialog is open
|
||||
const {
|
||||
@@ -76,17 +92,17 @@ export function InviteMembersDialogContainer({
|
||||
} = useFetchInvitationsPolicies({ accountSlug, isOpen });
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen} modal>
|
||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen} disablePointerDismissal>
|
||||
<DialogTrigger render={children as React.ReactElement} />
|
||||
|
||||
<DialogContent onInteractOutside={(e) => e.preventDefault()}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<Trans i18nKey={'teams:inviteMembersHeading'} />
|
||||
<Trans i18nKey={'teams.inviteMembersHeading'} />
|
||||
</DialogTitle>
|
||||
|
||||
<DialogDescription>
|
||||
<Trans i18nKey={'teams:inviteMembersDescription'} />
|
||||
<Trans i18nKey={'teams.inviteMembersDescription'} />
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -95,7 +111,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>
|
||||
@@ -104,7 +120,7 @@ export function InviteMembersDialogContainer({
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription>
|
||||
<Trans
|
||||
i18nKey="teams:policyCheckError"
|
||||
i18nKey="teams.policyCheckError"
|
||||
values={{ error: policiesError?.message }}
|
||||
/>
|
||||
</AlertDescription>
|
||||
@@ -126,28 +142,12 @@ export function InviteMembersDialogContainer({
|
||||
<RolesDataProvider maxRoleHierarchy={userRoleHierarchy}>
|
||||
{(roles) => (
|
||||
<InviteMembersForm
|
||||
pending={pending}
|
||||
pending={isPending}
|
||||
roles={roles}
|
||||
onSubmit={(data) => {
|
||||
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);
|
||||
execute({
|
||||
accountSlug,
|
||||
invitations: data.invitations,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
@@ -168,7 +168,7 @@ function InviteMembersForm({
|
||||
pending: boolean;
|
||||
roles: string[];
|
||||
}) {
|
||||
const { t } = useTranslation('teams');
|
||||
const t = useTranslations('teams');
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(InviteMembersSchema),
|
||||
@@ -237,7 +237,9 @@ function InviteMembersForm({
|
||||
roles={roles}
|
||||
value={field.value}
|
||||
onChange={(role) => {
|
||||
form.setValue(field.name, role);
|
||||
if (role) {
|
||||
form.setValue(field.name, role);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
@@ -251,22 +253,24 @@ function InviteMembersForm({
|
||||
<div className={'flex items-end justify-end'}>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<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>
|
||||
<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>
|
||||
}
|
||||
/>
|
||||
|
||||
<TooltipContent>
|
||||
{t('removeInviteButtonLabel')}
|
||||
@@ -294,7 +298,7 @@ function InviteMembersForm({
|
||||
<Plus className={'mr-1 h-3'} />
|
||||
|
||||
<span>
|
||||
<Trans i18nKey={'teams:addAnotherMemberButtonLabel'} />
|
||||
<Trans i18nKey={'teams.addAnotherMemberButtonLabel'} />
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
@@ -305,8 +309,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) => unknown;
|
||||
onChange: (role: Role | null) => unknown;
|
||||
triggerClassName?: string;
|
||||
}) {
|
||||
return (
|
||||
@@ -28,7 +28,15 @@ export function MembershipRoleSelector({
|
||||
className={triggerClassName}
|
||||
data-test={'role-selector-trigger'}
|
||||
>
|
||||
<SelectValue />
|
||||
<SelectValue>
|
||||
{(value) =>
|
||||
value ? (
|
||||
<Trans i18nKey={`common.roles.${value}.label`} defaults={value} />
|
||||
) : (
|
||||
''
|
||||
)
|
||||
}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
@@ -41,7 +49,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,4 +1,6 @@
|
||||
import { useState, useTransition } from 'react';
|
||||
'use client';
|
||||
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||
import {
|
||||
@@ -9,7 +11,6 @@ import {
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from '@kit/ui/alert-dialog';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { If } from '@kit/ui/if';
|
||||
@@ -18,29 +19,34 @@ import { Trans } from '@kit/ui/trans';
|
||||
import { removeMemberFromAccountAction } from '../../server/actions/team-members-server-actions';
|
||||
|
||||
export function RemoveMemberDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
teamAccountId,
|
||||
userId,
|
||||
children,
|
||||
}: React.PropsWithChildren<{
|
||||
}: {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
teamAccountId: string;
|
||||
userId: string;
|
||||
}>) {
|
||||
}) {
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>{children}</AlertDialogTrigger>
|
||||
|
||||
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||
<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} />
|
||||
<RemoveMemberForm
|
||||
accountId={teamAccountId}
|
||||
userId={userId}
|
||||
onSuccess={() => onOpenChange(false)}
|
||||
/>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
@@ -49,45 +55,46 @@ export function RemoveMemberDialog({
|
||||
function RemoveMemberForm({
|
||||
accountId,
|
||||
userId,
|
||||
onSuccess,
|
||||
}: {
|
||||
accountId: string;
|
||||
userId: string;
|
||||
onSuccess: () => void;
|
||||
}) {
|
||||
const [isSubmitting, startTransition] = useTransition();
|
||||
const [error, setError] = useState<boolean>();
|
||||
|
||||
const onMemberRemoved = () => {
|
||||
startTransition(async () => {
|
||||
try {
|
||||
await removeMemberFromAccountAction({ accountId, userId });
|
||||
} catch {
|
||||
setError(true);
|
||||
}
|
||||
});
|
||||
};
|
||||
const { execute, isPending, hasErrored } = useAction(
|
||||
removeMemberFromAccountAction,
|
||||
{
|
||||
onSuccess: () => onSuccess(),
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<form action={onMemberRemoved}>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
execute({ accountId, userId });
|
||||
}}
|
||||
>
|
||||
<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={error}>
|
||||
<If condition={hasErrored}>
|
||||
<RemoveMemberErrorAlert />
|
||||
</If>
|
||||
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>
|
||||
<Trans i18nKey={'common:cancel'} />
|
||||
<Trans i18nKey={'common.cancel'} />
|
||||
</AlertDialogCancel>
|
||||
|
||||
<Button
|
||||
data-test={'confirm-remove-member'}
|
||||
variant={'destructive'}
|
||||
disabled={isSubmitting}
|
||||
disabled={isPending}
|
||||
>
|
||||
<Trans i18nKey={'teams:removeMemberSubmitLabel'} />
|
||||
<Trans i18nKey={'teams.removeMemberSubmitLabel'} />
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</div>
|
||||
@@ -99,11 +106,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,8 +1,7 @@
|
||||
'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';
|
||||
@@ -16,7 +15,6 @@ import {
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from '@kit/ui/alert-dialog';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { Form } from '@kit/ui/form';
|
||||
@@ -27,30 +25,28 @@ import { TransferOwnershipConfirmationSchema } from '../../schema/transfer-owner
|
||||
import { transferOwnershipAction } from '../../server/actions/team-members-server-actions';
|
||||
|
||||
export function TransferOwnershipDialog({
|
||||
children,
|
||||
open,
|
||||
onOpenChange,
|
||||
targetDisplayName,
|
||||
accountId,
|
||||
userId,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
accountId: string;
|
||||
userId: string;
|
||||
targetDisplayName: string;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={setOpen}>
|
||||
<AlertDialogTrigger asChild>{children}</AlertDialogTrigger>
|
||||
|
||||
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
<Trans i18nKey="team:transferOwnership" />
|
||||
<Trans i18nKey="teams.transferOwnership" />
|
||||
</AlertDialogTitle>
|
||||
|
||||
<AlertDialogDescription>
|
||||
<Trans i18nKey="team:transferOwnershipDescription" />
|
||||
<Trans i18nKey="teams.transferOwnershipDescription" />
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
|
||||
@@ -58,7 +54,7 @@ export function TransferOwnershipDialog({
|
||||
accountId={accountId}
|
||||
userId={userId}
|
||||
targetDisplayName={targetDisplayName}
|
||||
onSuccess={() => setOpen(false)}
|
||||
onSuccess={() => onOpenChange(false)}
|
||||
/>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
@@ -76,10 +72,15 @@ 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: {
|
||||
@@ -103,7 +104,7 @@ function TransferOrganizationOwnershipForm({
|
||||
}}
|
||||
CancelButton={
|
||||
<AlertDialogCancel>
|
||||
<Trans i18nKey={'common:cancel'} />
|
||||
<Trans i18nKey={'common.cancel'} />
|
||||
</AlertDialogCancel>
|
||||
}
|
||||
data-test="verify-otp-form"
|
||||
@@ -117,25 +118,17 @@ function TransferOrganizationOwnershipForm({
|
||||
<form
|
||||
className={'flex flex-col space-y-4 text-sm'}
|
||||
onSubmit={form.handleSubmit((data) => {
|
||||
startTransition(async () => {
|
||||
try {
|
||||
await transferOwnershipAction(data);
|
||||
|
||||
onSuccess();
|
||||
} catch {
|
||||
setError(true);
|
||||
}
|
||||
});
|
||||
execute(data);
|
||||
})}
|
||||
>
|
||||
<If condition={error}>
|
||||
<If condition={hasErrored}>
|
||||
<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,
|
||||
}}
|
||||
@@ -148,26 +141,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={pending}
|
||||
disabled={isPending}
|
||||
>
|
||||
<If
|
||||
condition={pending}
|
||||
fallback={<Trans i18nKey={'teams:transferOwnership'} />}
|
||||
condition={isPending}
|
||||
fallback={<Trans i18nKey={'teams.transferOwnership'} />}
|
||||
>
|
||||
<Trans i18nKey={'teams:transferringOwnership'} />
|
||||
<Trans i18nKey={'teams.transferringOwnership'} />
|
||||
</If>
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
@@ -180,11 +173,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,10 +1,12 @@
|
||||
import { useState, useTransition } from 'react';
|
||||
'use client';
|
||||
|
||||
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,
|
||||
@@ -12,7 +14,6 @@ import {
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@kit/ui/dialog';
|
||||
import {
|
||||
Form,
|
||||
@@ -34,31 +35,30 @@ import { RolesDataProvider } from './roles-data-provider';
|
||||
type Role = string;
|
||||
|
||||
export function UpdateMemberRoleDialog({
|
||||
children,
|
||||
open,
|
||||
onOpenChange,
|
||||
userId,
|
||||
teamAccountId,
|
||||
userRole,
|
||||
userRoleHierarchy,
|
||||
}: React.PropsWithChildren<{
|
||||
}: {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
userId: string;
|
||||
teamAccountId: string;
|
||||
userRole: Role;
|
||||
userRoleHierarchy: number;
|
||||
}>) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
}) {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||
|
||||
<Dialog open={open} onOpenChange={onOpenChange} disablePointerDismissal>
|
||||
<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={() => setOpen(false)}
|
||||
onSuccess={() => onOpenChange(false)}
|
||||
/>
|
||||
)}
|
||||
</RolesDataProvider>
|
||||
@@ -91,25 +91,11 @@ function UpdateMemberForm({
|
||||
roles: Role[];
|
||||
onSuccess: () => unknown;
|
||||
}>) {
|
||||
const [pending, startTransition] = useTransition();
|
||||
const [error, setError] = useState<boolean>();
|
||||
const { t } = useTranslation('teams');
|
||||
const t = useTranslations('teams');
|
||||
|
||||
const onSubmit = ({ role }: { role: Role }) => {
|
||||
startTransition(async () => {
|
||||
try {
|
||||
await updateMemberRoleAction({
|
||||
accountId: teamAccountId,
|
||||
userId,
|
||||
role,
|
||||
});
|
||||
|
||||
onSuccess();
|
||||
} catch {
|
||||
setError(true);
|
||||
}
|
||||
});
|
||||
};
|
||||
const { execute, isPending, hasErrored } = useAction(updateMemberRoleAction, {
|
||||
onSuccess: () => onSuccess(),
|
||||
});
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(
|
||||
@@ -134,10 +120,16 @@ function UpdateMemberForm({
|
||||
<Form {...form}>
|
||||
<form
|
||||
data-test={'update-member-role-form'}
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className={'flex flex-col space-y-6'}
|
||||
onSubmit={form.handleSubmit(({ role }) => {
|
||||
execute({
|
||||
accountId: teamAccountId,
|
||||
userId,
|
||||
role,
|
||||
});
|
||||
})}
|
||||
className={'flex w-full flex-col space-y-6'}
|
||||
>
|
||||
<If condition={error}>
|
||||
<If condition={hasErrored}>
|
||||
<UpdateRoleErrorAlert />
|
||||
</If>
|
||||
|
||||
@@ -150,10 +142,15 @@ function UpdateMemberForm({
|
||||
|
||||
<FormControl>
|
||||
<MembershipRoleSelector
|
||||
triggerClassName={'w-full'}
|
||||
roles={roles}
|
||||
currentUserRole={userRole}
|
||||
value={field.value}
|
||||
onChange={(newRole) => form.setValue('role', newRole)}
|
||||
onChange={(newRole) => {
|
||||
if (newRole) {
|
||||
form.setValue('role', newRole);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
@@ -165,9 +162,19 @@ function UpdateMemberForm({
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button data-test={'confirm-update-member-role'} disabled={pending}>
|
||||
<Trans i18nKey={'teams:updateRoleSubmitLabel'} />
|
||||
</Button>
|
||||
<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>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
@@ -177,11 +184,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,10 +1,9 @@
|
||||
'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 { z } from 'zod';
|
||||
import * as z from 'zod';
|
||||
|
||||
import { ErrorBoundary } from '@kit/monitoring/components';
|
||||
import { VerifyOtpForm } from '@kit/otp/components';
|
||||
@@ -100,12 +99,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,
|
||||
}}
|
||||
@@ -115,25 +114,27 @@ function DeleteTeamContainer(props: {
|
||||
|
||||
<div>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
data-test={'delete-team-trigger'}
|
||||
type={'button'}
|
||||
variant={'destructive'}
|
||||
>
|
||||
<Trans i18nKey={'teams:deleteTeam'} />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogTrigger
|
||||
render={
|
||||
<Button
|
||||
data-test={'delete-team-trigger'}
|
||||
type={'button'}
|
||||
variant={'destructive'}
|
||||
>
|
||||
<Trans i18nKey={'teams.deleteTeam'} />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<AlertDialogContent onEscapeKeyDown={(e) => e.preventDefault()}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
<Trans i18nKey={'teams:deletingTeam'} />
|
||||
<Trans i18nKey={'teams.deletingTeam'} />
|
||||
</AlertDialogTitle>
|
||||
|
||||
<AlertDialogDescription>
|
||||
<Trans
|
||||
i18nKey={'teams:deletingTeamDescription'}
|
||||
i18nKey={'teams.deletingTeamDescription'}
|
||||
values={{
|
||||
teamName: props.account.name,
|
||||
}}
|
||||
@@ -161,6 +162,8 @@ function DeleteTeamConfirmationForm({
|
||||
}) {
|
||||
const { data: user } = useUser();
|
||||
|
||||
const { execute, isPending } = useAction(deleteTeamAccountAction);
|
||||
|
||||
const form = useForm({
|
||||
mode: 'onChange',
|
||||
reValidateMode: 'onChange',
|
||||
@@ -188,7 +191,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>
|
||||
}
|
||||
/>
|
||||
@@ -201,7 +204,10 @@ function DeleteTeamConfirmationForm({
|
||||
<form
|
||||
data-test={'delete-team-form'}
|
||||
className={'flex flex-col space-y-4'}
|
||||
action={deleteTeamAccountAction}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
execute({ accountId: id, otp });
|
||||
}}
|
||||
>
|
||||
<div className={'flex flex-col space-y-2'}>
|
||||
<div
|
||||
@@ -211,7 +217,7 @@ function DeleteTeamConfirmationForm({
|
||||
>
|
||||
<div>
|
||||
<Trans
|
||||
i18nKey={'teams:deleteTeamDisclaimer'}
|
||||
i18nKey={'teams.deleteTeamDisclaimer'}
|
||||
values={{
|
||||
teamName: name,
|
||||
}}
|
||||
@@ -219,20 +225,24 @@ 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>
|
||||
|
||||
<DeleteTeamSubmitButton />
|
||||
<Button
|
||||
type="submit"
|
||||
data-test={'delete-team-form-confirm-button'}
|
||||
disabled={isPending}
|
||||
variant={'destructive'}
|
||||
>
|
||||
<Trans i18nKey={'teams.deleteTeam'} />
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
@@ -240,26 +250,14 @@ 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({
|
||||
@@ -278,7 +276,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,
|
||||
}}
|
||||
@@ -286,26 +284,26 @@ function LeaveTeamContainer(props: {
|
||||
</p>
|
||||
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<div>
|
||||
<AlertDialogTrigger
|
||||
render={
|
||||
<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>
|
||||
|
||||
@@ -313,21 +311,20 @@ function LeaveTeamContainer(props: {
|
||||
<Form {...form}>
|
||||
<form
|
||||
className={'flex flex-col space-y-4'}
|
||||
action={leaveTeamAccountAction}
|
||||
onSubmit={form.handleSubmit((data) => {
|
||||
execute({
|
||||
accountId: props.account.id,
|
||||
confirmation: data.confirmation,
|
||||
});
|
||||
})}
|
||||
>
|
||||
<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>
|
||||
@@ -344,7 +341,7 @@ function LeaveTeamContainer(props: {
|
||||
</FormControl>
|
||||
|
||||
<FormDescription>
|
||||
<Trans i18nKey={'teams:leaveTeamInputDescription'} />
|
||||
<Trans i18nKey={'teams.leaveTeamInputDescription'} />
|
||||
</FormDescription>
|
||||
|
||||
<FormMessage />
|
||||
@@ -355,10 +352,17 @@ function LeaveTeamContainer(props: {
|
||||
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>
|
||||
<Trans i18nKey={'common:cancel'} />
|
||||
<Trans i18nKey={'common.cancel'} />
|
||||
</AlertDialogCancel>
|
||||
|
||||
<LeaveTeamSubmitButton />
|
||||
<Button
|
||||
type="submit"
|
||||
data-test={'confirm-leave-organization-button'}
|
||||
disabled={isPending}
|
||||
variant={'destructive'}
|
||||
>
|
||||
<Trans i18nKey={'teams.leaveTeam'} />
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
@@ -369,36 +373,22 @@ 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>
|
||||
@@ -410,17 +400,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>
|
||||
@@ -432,11 +422,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 { useTranslation } from 'react-i18next';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
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 } = useTranslation('teams');
|
||||
const t = useTranslations('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,13 +1,10 @@
|
||||
'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 {
|
||||
@@ -40,8 +37,7 @@ export const UpdateTeamAccountNameForm = (props: {
|
||||
|
||||
path: string;
|
||||
}) => {
|
||||
const [pending, startTransition] = useTransition();
|
||||
const { t } = useTranslation('teams');
|
||||
const t = useTranslations('teams');
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(TeamNameFormSchema),
|
||||
@@ -51,6 +47,21 @@ 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 || '');
|
||||
|
||||
@@ -61,41 +72,11 @@ export const UpdateTeamAccountNameForm = (props: {
|
||||
data-test={'update-team-account-name-form'}
|
||||
className={'flex flex-col space-y-4'}
|
||||
onSubmit={form.handleSubmit((data) => {
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
execute({
|
||||
slug: props.account.slug,
|
||||
name: data.name,
|
||||
newSlug: data.newSlug || undefined,
|
||||
path: props.path,
|
||||
});
|
||||
})}
|
||||
>
|
||||
@@ -105,7 +86,7 @@ export const UpdateTeamAccountNameForm = (props: {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans i18nKey={'teams:teamNameLabel'} />
|
||||
<Trans i18nKey={'teams.teamNameLabel'} />
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
@@ -117,7 +98,7 @@ export const UpdateTeamAccountNameForm = (props: {
|
||||
<InputGroupInput
|
||||
data-test={'team-name-input'}
|
||||
required
|
||||
placeholder={t('teams:teamNameInputLabel')}
|
||||
placeholder={t('teamNameInputLabel')}
|
||||
{...field}
|
||||
/>
|
||||
</InputGroup>
|
||||
@@ -136,7 +117,7 @@ export const UpdateTeamAccountNameForm = (props: {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans i18nKey={'teams:teamSlugLabel'} />
|
||||
<Trans i18nKey={'teams.teamSlugLabel'} />
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
@@ -155,7 +136,7 @@ export const UpdateTeamAccountNameForm = (props: {
|
||||
</FormControl>
|
||||
|
||||
<FormDescription>
|
||||
<Trans i18nKey={'teams:teamSlugDescription'} />
|
||||
<Trans i18nKey={'teams.teamSlugDescription'} />
|
||||
</FormDescription>
|
||||
|
||||
<FormMessage />
|
||||
@@ -167,11 +148,12 @@ export const UpdateTeamAccountNameForm = (props: {
|
||||
|
||||
<div>
|
||||
<Button
|
||||
type="submit"
|
||||
className={'w-full md:w-auto'}
|
||||
data-test={'update-team-submit-button'}
|
||||
disabled={pending}
|
||||
disabled={isPending}
|
||||
>
|
||||
<Trans i18nKey={'teams:updateTeamSubmitLabel'} />
|
||||
<Trans i18nKey={'teams.updateTeamSubmitLabel'} />
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { z } from 'zod';
|
||||
import * as z from 'zod';
|
||||
|
||||
export const AcceptInvitationSchema = z.object({
|
||||
inviteToken: z.string().uuid(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { z } from 'zod';
|
||||
import * as z from 'zod';
|
||||
|
||||
/**
|
||||
* @name RESERVED_NAMES_ARRAY
|
||||
@@ -40,20 +40,18 @@ export function containsNonLatinCharacters(value: string): boolean {
|
||||
* @description Schema for validating URL-friendly slugs
|
||||
*/
|
||||
export const SlugSchema = z
|
||||
.string({
|
||||
description: 'URL-friendly identifier for the team',
|
||||
})
|
||||
.string()
|
||||
.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',
|
||||
},
|
||||
);
|
||||
|
||||
@@ -62,9 +60,7 @@ export const SlugSchema = z
|
||||
* @description Schema for team name - allows non-Latin characters
|
||||
*/
|
||||
export const TeamNameSchema = z
|
||||
.string({
|
||||
description: 'The name of the team account',
|
||||
})
|
||||
.string()
|
||||
.min(2)
|
||||
.max(50)
|
||||
.refine(
|
||||
@@ -72,7 +68,7 @@ export const TeamNameSchema = z
|
||||
return !SPECIAL_CHARACTERS_REGEX.test(name);
|
||||
},
|
||||
{
|
||||
message: 'teams:specialCharactersError',
|
||||
message: 'teams.specialCharactersError',
|
||||
},
|
||||
)
|
||||
.refine(
|
||||
@@ -80,7 +76,7 @@ export const TeamNameSchema = z
|
||||
return !RESERVED_NAMES_ARRAY.includes(name.toLowerCase());
|
||||
},
|
||||
{
|
||||
message: 'teams:reservedNameError',
|
||||
message: 'teams.reservedNameError',
|
||||
},
|
||||
);
|
||||
|
||||
@@ -93,10 +89,11 @@ export const CreateTeamSchema = z
|
||||
.object({
|
||||
name: TeamNameSchema,
|
||||
// Transform empty strings to undefined before validation
|
||||
slug: z.preprocess(
|
||||
(val) => (val === '' ? undefined : val),
|
||||
SlugSchema.optional(),
|
||||
),
|
||||
slug: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((val) => (val === '' ? undefined : val))
|
||||
.pipe(SlugSchema.optional()),
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
@@ -107,7 +104,7 @@ export const CreateTeamSchema = z
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: 'teams:slugRequiredForNonLatinName',
|
||||
message: 'teams.slugRequiredForNonLatinName',
|
||||
path: ['slug'],
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { z } from 'zod';
|
||||
import * as z from 'zod';
|
||||
|
||||
export const DeleteInvitationSchema = z.object({
|
||||
invitationId: z.number().int(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { z } from 'zod';
|
||||
import * as z from 'zod';
|
||||
|
||||
export const DeleteTeamAccountSchema = z.object({
|
||||
accountId: z.string().uuid(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { z } from 'zod';
|
||||
import * as z from 'zod';
|
||||
|
||||
const InviteSchema = z.object({
|
||||
email: z.string().email(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { z } from 'zod';
|
||||
import * as z from 'zod';
|
||||
|
||||
export const LeaveTeamAccountSchema = z.object({
|
||||
accountId: z.string().uuid(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { z } from 'zod';
|
||||
import * as z from 'zod';
|
||||
|
||||
export const RemoveMemberSchema = z.object({
|
||||
accountId: z.string().uuid(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { z } from 'zod';
|
||||
import * as z from 'zod';
|
||||
|
||||
export const RenewInvitationSchema = z.object({
|
||||
invitationId: z.number().positive(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { z } from 'zod';
|
||||
import * as 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.infer<
|
||||
export type TransferOwnershipConfirmationData = z.output<
|
||||
typeof TransferOwnershipConfirmationSchema
|
||||
>;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { z } from 'zod';
|
||||
import * as z from 'zod';
|
||||
|
||||
export const UpdateInvitationSchema = z.object({
|
||||
invitationId: z.number(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { z } from 'zod';
|
||||
import * as z from 'zod';
|
||||
|
||||
export const RoleSchema = z.object({
|
||||
role: z.string().min(1),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { z } from 'zod';
|
||||
import * as z from 'zod';
|
||||
|
||||
import {
|
||||
SlugSchema,
|
||||
@@ -23,7 +23,7 @@ export const TeamNameFormSchema = z
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: 'teams:slugRequiredForNonLatinName',
|
||||
message: 'teams.slugRequiredForNonLatinName',
|
||||
path: ['newSlug'],
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
'use server';
|
||||
|
||||
import 'server-only';
|
||||
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
import { enhanceAction } from '@kit/next/actions';
|
||||
import { authActionClient } from '@kit/next/safe-action';
|
||||
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 = enhanceAction(
|
||||
async ({ name, slug }, user) => {
|
||||
export const createTeamAccountAction = authActionClient
|
||||
.schema(CreateTeamSchema)
|
||||
.action(async ({ parsedInput: { name, slug }, ctx: { user } }) => {
|
||||
const logger = await getLogger();
|
||||
const service = createCreateTeamAccountService();
|
||||
|
||||
@@ -61,7 +60,7 @@ export const createTeamAccountAction = enhanceAction(
|
||||
if (error === 'duplicate_slug') {
|
||||
return {
|
||||
error: true,
|
||||
message: 'teams:duplicateSlugError',
|
||||
message: 'teams.duplicateSlugError',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -70,8 +69,4 @@ export const createTeamAccountAction = enhanceAction(
|
||||
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 { enhanceAction } from '@kit/next/actions';
|
||||
import { authActionClient } from '@kit/next/safe-action';
|
||||
import { createOtpApi } from '@kit/otp';
|
||||
import { getLogger } from '@kit/shared/logger';
|
||||
import type { Database } from '@kit/supabase/database';
|
||||
@@ -16,14 +16,11 @@ import { createDeleteTeamAccountService } from '../services/delete-team-account.
|
||||
const enableTeamAccountDeletion =
|
||||
process.env.NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_DELETION === 'true';
|
||||
|
||||
export const deleteTeamAccountAction = enhanceAction(
|
||||
async (formData: FormData, user) => {
|
||||
export const deleteTeamAccountAction = authActionClient
|
||||
.schema(DeleteTeamAccountSchema)
|
||||
.action(async ({ parsedInput: params, ctx: { user } }) => {
|
||||
const logger = await getLogger();
|
||||
|
||||
const params = DeleteTeamAccountSchema.parse(
|
||||
Object.fromEntries(formData.entries()),
|
||||
);
|
||||
|
||||
const otpService = createOtpApi(getSupabaseServerClient());
|
||||
|
||||
const otpResult = await otpService.verifyToken({
|
||||
@@ -57,12 +54,8 @@ export const deleteTeamAccountAction = enhanceAction(
|
||||
|
||||
logger.info(ctx, `Team account request successfully sent`);
|
||||
|
||||
return redirect('/home');
|
||||
},
|
||||
{
|
||||
auth: true,
|
||||
},
|
||||
);
|
||||
redirect('/home');
|
||||
});
|
||||
|
||||
async function deleteTeamAccount(params: {
|
||||
accountId: string;
|
||||
|
||||
@@ -3,17 +3,15 @@
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
import { enhanceAction } from '@kit/next/actions';
|
||||
import { authActionClient } from '@kit/next/safe-action';
|
||||
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 = enhanceAction(
|
||||
async (formData: FormData, user) => {
|
||||
const body = Object.fromEntries(formData.entries());
|
||||
const params = LeaveTeamAccountSchema.parse(body);
|
||||
|
||||
export const leaveTeamAccountAction = authActionClient
|
||||
.schema(LeaveTeamAccountSchema)
|
||||
.action(async ({ parsedInput: params, ctx: { user } }) => {
|
||||
const service = createLeaveTeamAccountService(
|
||||
getSupabaseServerAdminClient(),
|
||||
);
|
||||
@@ -25,7 +23,5 @@ export const leaveTeamAccountAction = enhanceAction(
|
||||
|
||||
revalidatePath('/home/[account]', 'layout');
|
||||
|
||||
return redirect('/home');
|
||||
},
|
||||
{},
|
||||
);
|
||||
redirect('/home');
|
||||
});
|
||||
|
||||
@@ -2,14 +2,15 @@
|
||||
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
import { enhanceAction } from '@kit/next/actions';
|
||||
import { authActionClient } from '@kit/next/safe-action';
|
||||
import { getLogger } from '@kit/shared/logger';
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
|
||||
import { UpdateTeamNameSchema } from '../../schema/update-team-name.schema';
|
||||
|
||||
export const updateTeamAccountName = enhanceAction(
|
||||
async (params) => {
|
||||
export const updateTeamAccountName = authActionClient
|
||||
.schema(UpdateTeamNameSchema)
|
||||
.action(async ({ parsedInput: params }) => {
|
||||
const client = getSupabaseServerClient();
|
||||
const logger = await getLogger();
|
||||
const { name, path, slug, newSlug } = params;
|
||||
@@ -40,7 +41,7 @@ export const updateTeamAccountName = enhanceAction(
|
||||
if (error.code === '23505') {
|
||||
return {
|
||||
success: false,
|
||||
error: 'teams:duplicateSlugError',
|
||||
error: 'teams.duplicateSlugError',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -60,8 +61,4 @@ export const updateTeamAccountName = enhanceAction(
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
},
|
||||
{
|
||||
schema: UpdateTeamNameSchema,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
import { z } from 'zod';
|
||||
import * as z from 'zod';
|
||||
|
||||
import { enhanceAction } from '@kit/next/actions';
|
||||
import { authActionClient } from '@kit/next/safe-action';
|
||||
import { getLogger } from '@kit/shared/logger';
|
||||
import { Database } from '@kit/supabase/database';
|
||||
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
|
||||
@@ -26,8 +26,15 @@ import { createAccountPerSeatBillingService } from '../services/account-per-seat
|
||||
* @name createInvitationsAction
|
||||
* @description Creates invitations for inviting members.
|
||||
*/
|
||||
export const createInvitationsAction = enhanceAction(
|
||||
async (params, user) => {
|
||||
export const createInvitationsAction = authActionClient
|
||||
.schema(
|
||||
InviteMembersSchema.and(
|
||||
z.object({
|
||||
accountSlug: z.string().min(1),
|
||||
}),
|
||||
),
|
||||
)
|
||||
.action(async ({ parsedInput: params, ctx: { user } }) => {
|
||||
const logger = await getLogger();
|
||||
|
||||
logger.info(
|
||||
@@ -116,22 +123,15 @@ export const createInvitationsAction = enhanceAction(
|
||||
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 = enhanceAction(
|
||||
async (data) => {
|
||||
export const deleteInvitationAction = authActionClient
|
||||
.schema(DeleteInvitationSchema)
|
||||
.action(async ({ parsedInput: data }) => {
|
||||
const client = getSupabaseServerClient();
|
||||
const service = createAccountInvitationsService(client);
|
||||
|
||||
@@ -143,18 +143,15 @@ export const deleteInvitationAction = enhanceAction(
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
},
|
||||
{
|
||||
schema: DeleteInvitationSchema,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* @name updateInvitationAction
|
||||
* @description Updates an invitation.
|
||||
*/
|
||||
export const updateInvitationAction = enhanceAction(
|
||||
async (invitation) => {
|
||||
export const updateInvitationAction = authActionClient
|
||||
.schema(UpdateInvitationSchema)
|
||||
.action(async ({ parsedInput: invitation }) => {
|
||||
const client = getSupabaseServerClient();
|
||||
const service = createAccountInvitationsService(client);
|
||||
|
||||
@@ -165,23 +162,18 @@ export const updateInvitationAction = enhanceAction(
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
},
|
||||
{
|
||||
schema: UpdateInvitationSchema,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* @name acceptInvitationAction
|
||||
* @description Accepts an invitation to join a team.
|
||||
*/
|
||||
export const acceptInvitationAction = enhanceAction(
|
||||
async (data: FormData, user) => {
|
||||
export const acceptInvitationAction = authActionClient
|
||||
.schema(AcceptInvitationSchema)
|
||||
.action(async ({ parsedInput: data, ctx: { user } }) => {
|
||||
const client = getSupabaseServerClient();
|
||||
|
||||
const { inviteToken, nextPath } = AcceptInvitationSchema.parse(
|
||||
Object.fromEntries(data),
|
||||
);
|
||||
const { inviteToken, nextPath } = data;
|
||||
|
||||
// create the services
|
||||
const perSeatBillingService = createAccountPerSeatBillingService(client);
|
||||
@@ -205,19 +197,17 @@ export const acceptInvitationAction = enhanceAction(
|
||||
// Increase the seats for the account
|
||||
await perSeatBillingService.increaseSeats(accountId);
|
||||
|
||||
return redirect(nextPath);
|
||||
},
|
||||
{},
|
||||
);
|
||||
redirect(nextPath);
|
||||
});
|
||||
|
||||
/**
|
||||
* @name renewInvitationAction
|
||||
* @description Renews an invitation.
|
||||
*/
|
||||
export const renewInvitationAction = enhanceAction(
|
||||
async (params) => {
|
||||
export const renewInvitationAction = authActionClient
|
||||
.schema(RenewInvitationSchema)
|
||||
.action(async ({ parsedInput: { invitationId } }) => {
|
||||
const client = getSupabaseServerClient();
|
||||
const { invitationId } = RenewInvitationSchema.parse(params);
|
||||
|
||||
const service = createAccountInvitationsService(client);
|
||||
|
||||
@@ -229,11 +219,7 @@ export const renewInvitationAction = enhanceAction(
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
},
|
||||
{
|
||||
schema: RenewInvitationSchema,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
function revalidateMemberPage() {
|
||||
revalidatePath('/home/[account]/members', 'page');
|
||||
@@ -247,7 +233,7 @@ function revalidateMemberPage() {
|
||||
* @param accountId - The account ID (already fetched to avoid duplicate queries).
|
||||
*/
|
||||
async function evaluateInvitationsPolicies(
|
||||
params: z.infer<typeof InviteMembersSchema> & { accountSlug: string },
|
||||
params: z.output<typeof InviteMembersSchema> & { accountSlug: string },
|
||||
user: JWTUserData,
|
||||
accountId: string,
|
||||
) {
|
||||
@@ -282,7 +268,7 @@ async function evaluateInvitationsPolicies(
|
||||
async function checkInvitationPermissions(
|
||||
accountId: string,
|
||||
userId: string,
|
||||
invitations: z.infer<typeof InviteMembersSchema>['invitations'],
|
||||
invitations: z.output<typeof InviteMembersSchema>['invitations'],
|
||||
): Promise<{
|
||||
allowed: boolean;
|
||||
reason?: string;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { revalidatePath } from 'next/cache';
|
||||
|
||||
import { enhanceAction } from '@kit/next/actions';
|
||||
import { authActionClient } from '@kit/next/safe-action';
|
||||
import { createOtpApi } from '@kit/otp';
|
||||
import { getLogger } from '@kit/shared/logger';
|
||||
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
|
||||
@@ -17,8 +17,9 @@ import { createAccountMembersService } from '../services/account-members.service
|
||||
* @name removeMemberFromAccountAction
|
||||
* @description Removes a member from an account.
|
||||
*/
|
||||
export const removeMemberFromAccountAction = enhanceAction(
|
||||
async ({ accountId, userId }) => {
|
||||
export const removeMemberFromAccountAction = authActionClient
|
||||
.schema(RemoveMemberSchema)
|
||||
.action(async ({ parsedInput: { accountId, userId } }) => {
|
||||
const client = getSupabaseServerClient();
|
||||
const service = createAccountMembersService(client);
|
||||
|
||||
@@ -31,18 +32,15 @@ export const removeMemberFromAccountAction = enhanceAction(
|
||||
revalidatePath('/home/[account]', 'layout');
|
||||
|
||||
return { success: true };
|
||||
},
|
||||
{
|
||||
schema: RemoveMemberSchema,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* @name updateMemberRoleAction
|
||||
* @description Updates the role of a member in an account.
|
||||
*/
|
||||
export const updateMemberRoleAction = enhanceAction(
|
||||
async (data) => {
|
||||
export const updateMemberRoleAction = authActionClient
|
||||
.schema(UpdateMemberRoleSchema)
|
||||
.action(async ({ parsedInput: data }) => {
|
||||
const client = getSupabaseServerClient();
|
||||
const service = createAccountMembersService(client);
|
||||
const adminClient = getSupabaseServerAdminClient();
|
||||
@@ -54,19 +52,16 @@ export const updateMemberRoleAction = enhanceAction(
|
||||
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 = enhanceAction(
|
||||
async (data, user) => {
|
||||
export const transferOwnershipAction = authActionClient
|
||||
.schema(TransferOwnershipConfirmationSchema)
|
||||
.action(async ({ parsedInput: data, ctx: { user } }) => {
|
||||
const client = getSupabaseServerClient();
|
||||
const logger = await getLogger();
|
||||
|
||||
@@ -137,8 +132,4 @@ export const transferOwnershipAction = enhanceAction(
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
},
|
||||
{
|
||||
schema: TransferOwnershipConfirmationSchema,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { SupabaseClient } from '@supabase/supabase-js';
|
||||
|
||||
import { z } from 'zod';
|
||||
import * as 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.infer<typeof InviteMembersSchema> & { accountSlug: string },
|
||||
params: z.output<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.infer<typeof InviteMembersSchema> & { accountSlug: string },
|
||||
params: z.output<typeof InviteMembersSchema> & { accountSlug: string },
|
||||
user: JWTUserData,
|
||||
accountId: string,
|
||||
): Promise<FeaturePolicyInvitationContext> {
|
||||
|
||||
@@ -20,8 +20,8 @@ export const subscriptionRequiredInvitationsPolicy =
|
||||
if (!subscription || !subscription.active) {
|
||||
return deny({
|
||||
code: 'SUBSCRIPTION_REQUIRED',
|
||||
message: 'teams:policyErrors.subscriptionRequired',
|
||||
remediation: 'teams:policyRemediation.subscriptionRequired',
|
||||
message: 'teams.policyErrors.subscriptionRequired',
|
||||
remediation: 'teams.policyRemediation.subscriptionRequired',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -55,8 +55,8 @@ export const paddleBillingInvitationsPolicy =
|
||||
if (hasPerSeatItems) {
|
||||
return deny({
|
||||
code: 'PADDLE_TRIAL_RESTRICTION',
|
||||
message: 'teams:policyErrors.paddleTrialRestriction',
|
||||
remediation: 'teams:policyRemediation.paddleTrialRestriction',
|
||||
message: 'teams.policyErrors.paddleTrialRestriction',
|
||||
remediation: 'teams.policyRemediation.paddleTrialRestriction',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { SupabaseClient } from '@supabase/supabase-js';
|
||||
|
||||
import { z } from 'zod';
|
||||
import * as z from 'zod';
|
||||
|
||||
import { getLogger } from '@kit/shared/logger';
|
||||
import { Database, Tables } from '@kit/supabase/database';
|
||||
@@ -18,22 +18,22 @@ const env = z
|
||||
.object({
|
||||
invitePath: z
|
||||
.string({
|
||||
required_error: 'The property invitePath is required',
|
||||
error: 'The property invitePath is required',
|
||||
})
|
||||
.min(1),
|
||||
siteURL: z
|
||||
.string({
|
||||
required_error: 'NEXT_PUBLIC_SITE_URL is required',
|
||||
error: 'NEXT_PUBLIC_SITE_URL is required',
|
||||
})
|
||||
.min(1),
|
||||
productName: z
|
||||
.string({
|
||||
required_error: 'NEXT_PUBLIC_PRODUCT_NAME is required',
|
||||
error: 'NEXT_PUBLIC_PRODUCT_NAME is required',
|
||||
})
|
||||
.min(1),
|
||||
emailSender: z
|
||||
.string({
|
||||
required_error: 'EMAIL_SENDER is required',
|
||||
error: 'EMAIL_SENDER is required',
|
||||
})
|
||||
.min(1),
|
||||
})
|
||||
|
||||
@@ -3,7 +3,7 @@ import 'server-only';
|
||||
import { SupabaseClient } from '@supabase/supabase-js';
|
||||
|
||||
import { addDays, formatISO } from 'date-fns';
|
||||
import { z } from 'zod';
|
||||
import * as z from 'zod';
|
||||
|
||||
import { getLogger } from '@kit/shared/logger';
|
||||
import { Database } from '@kit/supabase/database';
|
||||
@@ -37,7 +37,7 @@ class AccountInvitationsService {
|
||||
* @description Removes an invitation from the database.
|
||||
* @param params
|
||||
*/
|
||||
async deleteInvitation(params: z.infer<typeof DeleteInvitationSchema>) {
|
||||
async deleteInvitation(params: z.output<typeof DeleteInvitationSchema>) {
|
||||
const logger = await getLogger();
|
||||
|
||||
const ctx = {
|
||||
@@ -70,7 +70,7 @@ class AccountInvitationsService {
|
||||
* @param params
|
||||
* @description Updates an invitation in the database.
|
||||
*/
|
||||
async updateInvitation(params: z.infer<typeof UpdateInvitationSchema>) {
|
||||
async updateInvitation(params: z.output<typeof UpdateInvitationSchema>) {
|
||||
const logger = await getLogger();
|
||||
|
||||
const ctx = {
|
||||
@@ -107,7 +107,7 @@ class AccountInvitationsService {
|
||||
}
|
||||
|
||||
async validateInvitation(
|
||||
invitation: z.infer<typeof InviteMembersSchema>['invitations'][number],
|
||||
invitation: z.output<typeof InviteMembersSchema>['invitations'][number],
|
||||
accountSlug: string,
|
||||
) {
|
||||
const { data: members, error } = await this.client.rpc(
|
||||
@@ -141,7 +141,7 @@ class AccountInvitationsService {
|
||||
invitations,
|
||||
invitedBy,
|
||||
}: {
|
||||
invitations: z.infer<typeof InviteMembersSchema>['invitations'];
|
||||
invitations: z.output<typeof InviteMembersSchema>['invitations'];
|
||||
accountSlug: string;
|
||||
invitedBy: string;
|
||||
}) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import 'server-only';
|
||||
|
||||
import { SupabaseClient } from '@supabase/supabase-js';
|
||||
|
||||
import { z } from 'zod';
|
||||
import * as z from 'zod';
|
||||
|
||||
import { getLogger } from '@kit/shared/logger';
|
||||
import { Database } from '@kit/supabase/database';
|
||||
@@ -26,7 +26,7 @@ class AccountMembersService {
|
||||
* @description Removes a member from an account.
|
||||
* @param params
|
||||
*/
|
||||
async removeMemberFromAccount(params: z.infer<typeof RemoveMemberSchema>) {
|
||||
async removeMemberFromAccount(params: z.output<typeof RemoveMemberSchema>) {
|
||||
const logger = await getLogger();
|
||||
|
||||
const ctx = {
|
||||
@@ -75,7 +75,7 @@ class AccountMembersService {
|
||||
* @param adminClient
|
||||
*/
|
||||
async updateMemberRole(
|
||||
params: z.infer<typeof UpdateMemberRoleSchema>,
|
||||
params: z.output<typeof UpdateMemberRoleSchema>,
|
||||
adminClient: SupabaseClient<Database>,
|
||||
) {
|
||||
const logger = await getLogger();
|
||||
@@ -145,7 +145,7 @@ class AccountMembersService {
|
||||
* @param adminClient
|
||||
*/
|
||||
async transferOwnership(
|
||||
params: z.infer<typeof TransferOwnershipConfirmationSchema>,
|
||||
params: z.output<typeof TransferOwnershipConfirmationSchema>,
|
||||
adminClient: SupabaseClient<Database>,
|
||||
) {
|
||||
const logger = await getLogger();
|
||||
|
||||
@@ -2,7 +2,7 @@ import 'server-only';
|
||||
|
||||
import { SupabaseClient } from '@supabase/supabase-js';
|
||||
|
||||
import { z } from 'zod';
|
||||
import * as z from 'zod';
|
||||
|
||||
import { getLogger } from '@kit/shared/logger';
|
||||
import { Database } from '@kit/supabase/database';
|
||||
@@ -32,7 +32,7 @@ class LeaveTeamAccountService {
|
||||
* @description Leave a team account
|
||||
* @param params
|
||||
*/
|
||||
async leaveTeamAccount(params: z.infer<typeof Schema>) {
|
||||
async leaveTeamAccount(params: z.output<typeof Schema>) {
|
||||
const logger = await getLogger();
|
||||
|
||||
const ctx = {
|
||||
|
||||
Reference in New Issue
Block a user