Remove team account related services and actions
Removed services and actions related to team account deletion as well as updated paths within other dependent files, better reflecting their new locations. Also, added a new service titled 'AccountBillingService' for handling billing-related operations and restructured the form layout and handled translation in 'team-account-danger-zone' component.
This commit is contained in:
@@ -6,7 +6,13 @@ import { z } from 'zod';
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { Dialog, DialogContent, DialogTitle } from '@kit/ui/dialog';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@kit/ui/dialog';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
@@ -20,8 +26,8 @@ import { If } from '@kit/ui/if';
|
||||
import { Input } from '@kit/ui/input';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { createOrganizationAccountAction } from '../actions/create-team-account-server-actions';
|
||||
import { CreateTeamSchema } from '../schema/create-team.schema';
|
||||
import { createOrganizationAccountAction } from '../server/actions/create-team-account-server-actions';
|
||||
|
||||
export function CreateTeamAccountDialog(
|
||||
props: React.PropsWithChildren<{
|
||||
@@ -31,18 +37,27 @@ export function CreateTeamAccountDialog(
|
||||
) {
|
||||
return (
|
||||
<Dialog open={props.isOpen} onOpenChange={props.setIsOpen}>
|
||||
<DialogContent>
|
||||
<DialogTitle>
|
||||
<Trans i18nKey={'teams:createOrganizationModalHeading'} />
|
||||
</DialogTitle>
|
||||
<DialogContent
|
||||
onEscapeKeyDown={(e) => e.preventDefault()}
|
||||
onInteractOutside={(e) => e.preventDefault()}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<Trans i18nKey={'teams:createTeamModalHeading'} />
|
||||
</DialogTitle>
|
||||
|
||||
<CreateOrganizationAccountForm />
|
||||
<DialogDescription>
|
||||
<Trans i18nKey={'teams:createTeamModalDescription'} />
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<CreateOrganizationAccountForm onClose={() => props.setIsOpen(false)} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function CreateOrganizationAccountForm() {
|
||||
function CreateOrganizationAccountForm(props: { onClose: () => void }) {
|
||||
const [error, setError] = useState<boolean>();
|
||||
const [pending, startTransition] = useTransition();
|
||||
|
||||
@@ -77,12 +92,12 @@ function CreateOrganizationAccountForm() {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans i18nKey={'teams:organizationNameLabel'} />
|
||||
<Trans i18nKey={'teams:teamNameLabel'} />
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Input
|
||||
data-test={'create-organization-name-input'}
|
||||
data-test={'create-team-name-input'}
|
||||
required
|
||||
minLength={2}
|
||||
maxLength={50}
|
||||
@@ -92,7 +107,7 @@ function CreateOrganizationAccountForm() {
|
||||
</FormControl>
|
||||
|
||||
<FormDescription>
|
||||
Your organization name should be unique and descriptive.
|
||||
<Trans i18nKey={'teams:teamNameDescription'} />
|
||||
</FormDescription>
|
||||
|
||||
<FormMessage />
|
||||
@@ -101,12 +116,20 @@ function CreateOrganizationAccountForm() {
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
data-test={'confirm-create-organization-button'}
|
||||
disabled={pending}
|
||||
>
|
||||
<Trans i18nKey={'teams:createOrganizationSubmitLabel'} />
|
||||
</Button>
|
||||
<div className={'flex justify-end space-x-2'}>
|
||||
<Button
|
||||
variant={'outline'}
|
||||
disabled={pending}
|
||||
type={'button'}
|
||||
onClick={props.onClose}
|
||||
>
|
||||
<Trans i18nKey={'common:cancel'} />
|
||||
</Button>
|
||||
|
||||
<Button data-test={'confirm-create-team-button'} disabled={pending}>
|
||||
<Trans i18nKey={'teams:createTeamSubmitLabel'} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
@@ -117,11 +140,11 @@ function CreateOrganizationErrorAlert() {
|
||||
return (
|
||||
<Alert variant={'destructive'}>
|
||||
<AlertTitle>
|
||||
<Trans i18nKey={'teams:createOrganizationErrorHeading'} />
|
||||
<Trans i18nKey={'teams:createTeamErrorHeading'} />
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
<Trans i18nKey={'teams:createOrganizationErrorMessage'} />
|
||||
<Trans i18nKey={'teams:createTeamErrorMessage'} />
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export * from './members/account-members-table';
|
||||
export * from './update-organization-form';
|
||||
export * from './members/invite-members-dialog-container';
|
||||
export * from './team-account-danger-zone';
|
||||
export * from './settings/team-account-danger-zone';
|
||||
export * from './invitations/account-invitations-table';
|
||||
export * from './settings/team-account-settings-container';
|
||||
|
||||
@@ -18,7 +18,7 @@ import { If } from '@kit/ui/if';
|
||||
import { Input } from '@kit/ui/input';
|
||||
import { ProfileAvatar } from '@kit/ui/profile-avatar';
|
||||
|
||||
import { RoleBadge } from '../role-badge';
|
||||
import { RoleBadge } from '../members/role-badge';
|
||||
import { DeleteInvitationDialog } from './delete-invitation-dialog';
|
||||
import { UpdateInvitationDialog } from './update-invitation-dialog';
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
import { If } from '@kit/ui/if';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { deleteInvitationAction } from '../../actions/account-invitations-server-actions';
|
||||
import { deleteInvitationAction } from '../../server/actions/team-invitations-server-actions';
|
||||
|
||||
export const DeleteInvitationDialog: React.FC<{
|
||||
isOpen: boolean;
|
||||
@@ -24,7 +24,7 @@ export const DeleteInvitationDialog: React.FC<{
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<Trans i18nKey="organization:deleteInvitationDialogTitle" />
|
||||
<Trans i18nKey="team:deleteInvitationDialogTitle" />
|
||||
</DialogTitle>
|
||||
|
||||
<DialogDescription>
|
||||
|
||||
@@ -25,9 +25,9 @@ import {
|
||||
import { If } from '@kit/ui/if';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { updateInvitationAction } from '../../actions/account-invitations-server-actions';
|
||||
import { UpdateRoleSchema } from '../../schema/update-role-schema';
|
||||
import { MembershipRoleSelector } from '../membership-role-selector';
|
||||
import { updateInvitationAction } from '../../server/actions/team-invitations-server-actions';
|
||||
import { MembershipRoleSelector } from '../members/membership-role-selector';
|
||||
|
||||
type Role = Database['public']['Enums']['account_role'];
|
||||
|
||||
|
||||
@@ -18,8 +18,8 @@ import { If } from '@kit/ui/if';
|
||||
import { Input } from '@kit/ui/input';
|
||||
import { ProfileAvatar } from '@kit/ui/profile-avatar';
|
||||
|
||||
import { RoleBadge } from '../role-badge';
|
||||
import { RemoveMemberDialog } from './remove-member-dialog';
|
||||
import { RoleBadge } from './role-badge';
|
||||
import { TransferOwnershipDialog } from './transfer-ownership-dialog';
|
||||
import { UpdateMemberRoleDialog } from './update-member-role-dialog';
|
||||
|
||||
|
||||
@@ -33,9 +33,9 @@ import {
|
||||
} from '@kit/ui/tooltip';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { createInvitationsAction } from '../../actions/account-invitations-server-actions';
|
||||
import { InviteMembersSchema } from '../../schema/invite-members.schema';
|
||||
import { MembershipRoleSelector } from '../membership-role-selector';
|
||||
import { createInvitationsAction } from '../../server/actions/team-invitations-server-actions';
|
||||
import { MembershipRoleSelector } from './membership-role-selector';
|
||||
|
||||
type InviteModel = ReturnType<typeof createEmptyInviteModel>;
|
||||
|
||||
@@ -59,8 +59,7 @@ export function InviteMembersDialogContainer({
|
||||
<DialogTitle>Invite Members to Organization</DialogTitle>
|
||||
|
||||
<DialogDescription>
|
||||
Invite members to your organization by entering their email and
|
||||
role.
|
||||
Invite members to your team by entering their email and role.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -89,7 +88,7 @@ function InviteMembersForm({
|
||||
onSubmit: (data: { invitations: InviteModel[] }) => void;
|
||||
pending: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation('organization');
|
||||
const { t } = useTranslation('team');
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(InviteMembersSchema),
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
import { If } from '@kit/ui/if';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { removeMemberFromAccountAction } from '../../actions/account-members-server-actions';
|
||||
import { removeMemberFromAccountAction } from '../../server/actions/team-members-server-actions';
|
||||
|
||||
export const RemoveMemberDialog: React.FC<{
|
||||
isOpen: boolean;
|
||||
@@ -25,11 +25,11 @@ export const RemoveMemberDialog: React.FC<{
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<Trans i18nKey="organization:removeMemberModalHeading" />
|
||||
<Trans i18nKey="team:removeMemberModalHeading" />
|
||||
</DialogTitle>
|
||||
|
||||
<DialogDescription>
|
||||
Remove this member from the organization.
|
||||
Remove this member from the team.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
|
||||
@@ -27,8 +27,8 @@ import { If } from '@kit/ui/if';
|
||||
import { Input } from '@kit/ui/input';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { transferOwnershipAction } from '../../actions/account-members-server-actions';
|
||||
import { TransferOwnershipConfirmationSchema } from '../../schema/transfer-ownership-confirmation.schema';
|
||||
import { transferOwnershipAction } from '../../server/actions/team-members-server-actions';
|
||||
|
||||
export const TransferOwnershipDialog: React.FC<{
|
||||
isOpen: boolean;
|
||||
@@ -42,11 +42,11 @@ export const TransferOwnershipDialog: React.FC<{
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<Trans i18nKey="organization:transferOwnership" />
|
||||
<Trans i18nKey="team:transferOwnership" />
|
||||
</DialogTitle>
|
||||
|
||||
<DialogDescription>
|
||||
Transfer ownership of the organization to another member.
|
||||
Transfer ownership of the team to another member.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
|
||||
@@ -25,9 +25,9 @@ import {
|
||||
import { If } from '@kit/ui/if';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { updateMemberRoleAction } from '../../actions/account-members-server-actions';
|
||||
import { UpdateRoleSchema } from '../../schema/update-role-schema';
|
||||
import { MembershipRoleSelector } from '../membership-role-selector';
|
||||
import { updateMemberRoleAction } from '../../server/actions/team-members-server-actions';
|
||||
import { MembershipRoleSelector } from './membership-role-selector';
|
||||
|
||||
type Role = Database['public']['Enums']['account_role'];
|
||||
|
||||
|
||||
@@ -0,0 +1,336 @@
|
||||
'use client';
|
||||
|
||||
import { useFormStatus } from 'react-dom';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from '@kit/ui/alert-dialog';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { ErrorBoundary } from '@kit/ui/error-boundary';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@kit/ui/form';
|
||||
import { Input } from '@kit/ui/input';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { deleteTeamAccountAction } from '../../server/actions/delete-team-account-server-actions';
|
||||
import { leaveTeamAccountAction } from '../../server/actions/leave-team-account-server-actions';
|
||||
|
||||
export function TeamAccountDangerZone({
|
||||
account,
|
||||
userIsPrimaryOwner,
|
||||
}: React.PropsWithChildren<{
|
||||
account: {
|
||||
name: string;
|
||||
id: string;
|
||||
};
|
||||
|
||||
userIsPrimaryOwner: boolean;
|
||||
}>) {
|
||||
if (userIsPrimaryOwner) {
|
||||
return <DeleteTeamContainer account={account} />;
|
||||
}
|
||||
|
||||
return <LeaveTeamContainer account={account} />;
|
||||
}
|
||||
|
||||
function DeleteTeamContainer(props: {
|
||||
account: {
|
||||
name: string;
|
||||
id: string;
|
||||
};
|
||||
}) {
|
||||
return (
|
||||
<div className={'flex flex-col space-y-4'}>
|
||||
<div className={'flex flex-col space-y-1'}>
|
||||
<span className={'font-medium'}>
|
||||
<Trans i18nKey={'teams:deleteTeam'} />
|
||||
</span>
|
||||
|
||||
<p className={'text-sm text-gray-500'}>
|
||||
<Trans
|
||||
i18nKey={'teams:deleteTeamDescription'}
|
||||
values={{
|
||||
teamName: props.account.name,
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
data-test={'delete-team-button'}
|
||||
type={'button'}
|
||||
variant={'destructive'}
|
||||
>
|
||||
<Trans i18nKey={'teams:deleteTeam'} />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
|
||||
<AlertDialogContent onEscapeKeyDown={(e) => e.preventDefault()}>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
<Trans i18nKey={'teams:deletingTeam'} />
|
||||
</AlertDialogTitle>
|
||||
|
||||
<AlertDialogDescription>
|
||||
<Trans
|
||||
i18nKey={'teams:deletingTeamDescription'}
|
||||
values={{
|
||||
teamName: props.account.name,
|
||||
}}
|
||||
/>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
|
||||
<DeleteTeamConfirmationForm
|
||||
name={props.account.name}
|
||||
id={props.account.id}
|
||||
/>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DeleteTeamConfirmationForm({
|
||||
name,
|
||||
id,
|
||||
}: {
|
||||
name: string;
|
||||
id: string;
|
||||
}) {
|
||||
const form = useForm({
|
||||
mode: 'onChange',
|
||||
reValidateMode: 'onChange',
|
||||
resolver: zodResolver(
|
||||
z.object({
|
||||
name: z.string().refine((value) => value === name, {
|
||||
message: 'Name does not match',
|
||||
path: ['name'],
|
||||
}),
|
||||
}),
|
||||
),
|
||||
defaultValues: {
|
||||
name: '',
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<ErrorBoundary fallback={<DeleteTeamErrorAlert />}>
|
||||
<Form {...form}>
|
||||
<form
|
||||
className={'flex flex-col space-y-4'}
|
||||
action={deleteTeamAccountAction}
|
||||
>
|
||||
<div className={'flex flex-col space-y-2'}>
|
||||
<div
|
||||
className={
|
||||
'border-2 border-red-500 p-4 text-sm text-red-500' +
|
||||
' flex flex-col space-y-2'
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<Trans
|
||||
i18nKey={'teams:deleteTeamDisclaimer'}
|
||||
values={{
|
||||
teamName: name,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={'text-sm'}>
|
||||
<Trans i18nKey={'common:modalConfirmationQuestion'} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="hidden" value={id} name={'accountId'} />
|
||||
|
||||
<FormField
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans i18nKey={'teams:teamNameInputLabel'} />
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Input
|
||||
data-test={'delete-team-input-field'}
|
||||
required
|
||||
type={'text'}
|
||||
className={'w-full'}
|
||||
placeholder={''}
|
||||
pattern={name}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormDescription>
|
||||
<Trans i18nKey={'teams:deleteTeamInputField'} />
|
||||
</FormDescription>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
name={'confirm'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>
|
||||
<Trans i18nKey={'common:cancel'} />
|
||||
</AlertDialogCancel>
|
||||
|
||||
<DeleteTeamSubmitButton />
|
||||
</AlertDialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
function DeleteTeamSubmitButton() {
|
||||
const { pending } = useFormStatus();
|
||||
|
||||
return (
|
||||
<Button
|
||||
data-test={'confirm-delete-team-button'}
|
||||
disabled={pending}
|
||||
variant={'destructive'}
|
||||
>
|
||||
<Trans i18nKey={'teams:deleteTeam'} />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function LeaveTeamContainer(props: {
|
||||
account: {
|
||||
name: string;
|
||||
id: string;
|
||||
};
|
||||
}) {
|
||||
return (
|
||||
<div className={'flex flex-col space-y-4'}>
|
||||
<p>
|
||||
<Trans
|
||||
i18nKey={'teams:leaveTeamDescription'}
|
||||
values={{
|
||||
teamName: props.account.name,
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
data-test={'leave-team-button'}
|
||||
type={'button'}
|
||||
variant={'destructive'}
|
||||
>
|
||||
<Trans i18nKey={'teams:leaveTeam'} />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
<Trans i18nKey={'teams:leavingTeamModalHeading'} />
|
||||
</AlertDialogTitle>
|
||||
|
||||
<AlertDialogDescription>
|
||||
<Trans i18nKey={'teams:leavingTeamModalDescription'} />
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
|
||||
<ErrorBoundary fallback={<LeaveTeamErrorAlert />}>
|
||||
<form action={leaveTeamAccountAction}>
|
||||
<input type={'hidden'} value={props.account.id} name={'id'} />
|
||||
|
||||
<div className={'my-2 flex flex-col space-y-4'}>
|
||||
<Trans
|
||||
i18nKey={'teams:leaveTeamDisclaimer'}
|
||||
values={{
|
||||
teamName: props.account?.name,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</ErrorBoundary>
|
||||
</AlertDialogContent>
|
||||
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>
|
||||
<Trans i18nKey={'common:cancel'} />
|
||||
</AlertDialogCancel>
|
||||
|
||||
<LeaveTeamSubmitButton />
|
||||
</AlertDialogFooter>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LeaveTeamSubmitButton() {
|
||||
const { pending } = useFormStatus();
|
||||
|
||||
return (
|
||||
<Button
|
||||
data-test={'confirm-leave-organization-button'}
|
||||
disabled={pending}
|
||||
variant={'destructive'}
|
||||
>
|
||||
<Trans i18nKey={'teams:leaveOrganization'} />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function LeaveTeamErrorAlert() {
|
||||
return (
|
||||
<Alert variant={'destructive'}>
|
||||
<AlertTitle>
|
||||
<Trans i18nKey={'teams:leaveTeamErrorHeading'} />
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
<Trans i18nKey={'common:genericError'} />
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
function DeleteTeamErrorAlert() {
|
||||
return (
|
||||
<Alert variant={'destructive'}>
|
||||
<AlertTitle>
|
||||
<Trans i18nKey={'teams:deleteTeamErrorHeading'} />
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
<Trans i18nKey={'common:genericError'} />
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@kit/ui/card';
|
||||
|
||||
import { TeamAccountDangerZone } from './team-account-danger-zone';
|
||||
import { UpdateTeamAccountImage } from './update-team-account-image-container';
|
||||
import { UpdateTeamAccountNameForm } from './update-team-account-name-form';
|
||||
|
||||
export function TeamAccountSettingsContainer(props: {
|
||||
account: {
|
||||
name: string;
|
||||
slug: string;
|
||||
id: string;
|
||||
pictureUrl: string | null;
|
||||
primaryOwnerUserId: string;
|
||||
};
|
||||
|
||||
userId: string;
|
||||
|
||||
paths: {
|
||||
teamAccountSettings: string;
|
||||
};
|
||||
}) {
|
||||
return (
|
||||
<div className={'flex flex-col space-y-8'}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Team Logo</CardTitle>
|
||||
|
||||
<CardDescription>
|
||||
Update your team's logo to make it easier to identify
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<UpdateTeamAccountImage account={props.account} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Team Account Settings</CardTitle>
|
||||
|
||||
<CardDescription>Manage your team account settings</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<UpdateTeamAccountNameForm
|
||||
path={props.paths.teamAccountSettings}
|
||||
account={props.account}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className={'border-destructive border-2'}>
|
||||
<CardHeader>
|
||||
<CardTitle>Danger Zone</CardTitle>
|
||||
|
||||
<CardDescription>
|
||||
Please be careful when making changes in this section as they are
|
||||
irreversible.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<TeamAccountDangerZone
|
||||
userIsPrimaryOwner={
|
||||
props.userId === props.account.primaryOwnerUserId
|
||||
}
|
||||
account={props.account}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import type { SupabaseClient } from '@supabase/supabase-js';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { useSupabase } from '@kit/supabase/hooks/use-supabase';
|
||||
import { ImageUploader } from '@kit/ui/image-uploader';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
const AVATARS_BUCKET = 'account_image';
|
||||
|
||||
export function UpdateTeamAccountImage(props: {
|
||||
account: {
|
||||
id: string;
|
||||
name: string;
|
||||
pictureUrl: string | null;
|
||||
};
|
||||
}) {
|
||||
const client = useSupabase();
|
||||
const { t } = useTranslation('teams');
|
||||
|
||||
const createToaster = useCallback(
|
||||
(promise: () => Promise<unknown>) => {
|
||||
return toast.promise(promise, {
|
||||
success: t(`updateTeamSuccessMessage`),
|
||||
error: t(`updateTeamErrorMessage`),
|
||||
loading: t(`updateTeamLoadingMessage`),
|
||||
});
|
||||
},
|
||||
[t],
|
||||
);
|
||||
|
||||
const onValueChange = useCallback(
|
||||
(file: File | null) => {
|
||||
const removeExistingStorageFile = () => {
|
||||
if (props.account.pictureUrl) {
|
||||
return (
|
||||
deleteProfilePhoto(client, props.account.pictureUrl) ??
|
||||
Promise.resolve()
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
if (file) {
|
||||
const promise = () =>
|
||||
removeExistingStorageFile().then(() =>
|
||||
uploadUserProfilePhoto(client, file, props.account.id).then(
|
||||
(pictureUrl) => {
|
||||
return client
|
||||
.from('accounts')
|
||||
.update({
|
||||
picture_url: pictureUrl,
|
||||
})
|
||||
.eq('id', props.account.id)
|
||||
.throwOnError();
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
createToaster(promise);
|
||||
} else {
|
||||
const promise = () =>
|
||||
removeExistingStorageFile().then(() => {
|
||||
return client
|
||||
.from('accounts')
|
||||
.update({
|
||||
picture_url: null,
|
||||
})
|
||||
.eq('id', props.account.id)
|
||||
.throwOnError();
|
||||
});
|
||||
|
||||
createToaster(promise);
|
||||
}
|
||||
},
|
||||
[client, createToaster, props],
|
||||
);
|
||||
|
||||
return (
|
||||
<ImageUploader
|
||||
value={props.account.pictureUrl}
|
||||
onValueChange={onValueChange}
|
||||
>
|
||||
<div className={'flex flex-col space-y-1'}>
|
||||
<span className={'text-sm'}>
|
||||
<Trans i18nKey={'account:profilePictureHeading'} />
|
||||
</span>
|
||||
|
||||
<span className={'text-xs'}>
|
||||
<Trans i18nKey={'account:profilePictureSubheading'} />
|
||||
</span>
|
||||
</div>
|
||||
</ImageUploader>
|
||||
);
|
||||
}
|
||||
|
||||
function deleteProfilePhoto(client: SupabaseClient, url: string) {
|
||||
const bucket = client.storage.from(AVATARS_BUCKET);
|
||||
const fileName = url.split('/').pop()?.split('?')[0];
|
||||
|
||||
if (!fileName) {
|
||||
return;
|
||||
}
|
||||
|
||||
return bucket.remove([fileName]);
|
||||
}
|
||||
|
||||
async function uploadUserProfilePhoto(
|
||||
client: SupabaseClient,
|
||||
photoFile: File,
|
||||
userId: string,
|
||||
) {
|
||||
const bytes = await photoFile.arrayBuffer();
|
||||
const bucket = client.storage.from(AVATARS_BUCKET);
|
||||
const extension = photoFile.name.split('.').pop();
|
||||
const fileName = await getAvatarFileName(userId, extension);
|
||||
|
||||
const result = await bucket.upload(fileName, bytes);
|
||||
|
||||
if (!result.error) {
|
||||
return bucket.getPublicUrl(fileName).data.publicUrl;
|
||||
}
|
||||
|
||||
throw result.error;
|
||||
}
|
||||
|
||||
async function getAvatarFileName(
|
||||
userId: string,
|
||||
extension: string | undefined,
|
||||
) {
|
||||
const { nanoid } = await import('nanoid');
|
||||
const uniqueId = nanoid(16);
|
||||
|
||||
return `${userId}.${extension}?v=${uniqueId}`;
|
||||
}
|
||||
@@ -1,14 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback } from 'react';
|
||||
import { useTransition } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toast } from 'sonner';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { useUpdateAccountData } from '@kit/accounts/hooks/use-update-account';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import {
|
||||
Form,
|
||||
@@ -20,44 +17,42 @@ import {
|
||||
import { Input } from '@kit/ui/input';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { updateTeamAccountName } from '../../server/actions/team-details-server-actions';
|
||||
|
||||
const Schema = z.object({
|
||||
name: z.string().min(1).max(255),
|
||||
});
|
||||
|
||||
export const UpdateOrganizationForm = (props: {
|
||||
accountId: string;
|
||||
accountName: string;
|
||||
export const UpdateTeamAccountNameForm = (props: {
|
||||
account: {
|
||||
name: string;
|
||||
slug: string;
|
||||
};
|
||||
|
||||
path: string;
|
||||
}) => {
|
||||
const updateAccountData = useUpdateAccountData(props.accountId);
|
||||
const { t } = useTranslation('organization');
|
||||
const [pending, startTransition] = useTransition();
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(Schema),
|
||||
defaultValues: {
|
||||
name: props.accountName,
|
||||
name: props.account.name,
|
||||
},
|
||||
});
|
||||
|
||||
const updateOrganizationData = useCallback(
|
||||
(data: { name: string }) => {
|
||||
const promise = updateAccountData.mutateAsync(data);
|
||||
|
||||
toast.promise(promise, {
|
||||
loading: t(`updateOrganizationLoadingMessage`),
|
||||
success: t(`updateOrganizationSuccessMessage`),
|
||||
error: t(`updateOrganizationErrorMessage`),
|
||||
});
|
||||
},
|
||||
[t, updateAccountData],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={'space-y-8'}>
|
||||
<Form {...form}>
|
||||
<form
|
||||
className={'flex flex-col space-y-4'}
|
||||
onSubmit={form.handleSubmit((data) => {
|
||||
updateOrganizationData(data);
|
||||
startTransition(async () => {
|
||||
await updateTeamAccountName({
|
||||
slug: props.account.slug,
|
||||
name: data.name,
|
||||
path: props.path,
|
||||
});
|
||||
});
|
||||
})}
|
||||
>
|
||||
<FormField
|
||||
@@ -66,12 +61,12 @@ export const UpdateOrganizationForm = (props: {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans i18nKey={'teams:organizationNameInputLabel'} />
|
||||
<Trans i18nKey={'teams:teamNameInputLabel'} />
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Input
|
||||
data-test={'organization-name-input'}
|
||||
data-test={'team-name-input'}
|
||||
required
|
||||
placeholder={''}
|
||||
{...field}
|
||||
@@ -80,15 +75,15 @@ export const UpdateOrganizationForm = (props: {
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
></FormField>
|
||||
/>
|
||||
|
||||
<div>
|
||||
<Button
|
||||
className={'w-full md:w-auto'}
|
||||
data-test={'update-organization-submit-button'}
|
||||
disabled={updateAccountData.isPending}
|
||||
data-test={'update-team-submit-button'}
|
||||
disabled={pending}
|
||||
>
|
||||
<Trans i18nKey={'teams:updateOrganizationSubmitLabel'} />
|
||||
<Trans i18nKey={'teams:updateTeamSubmitLabel'} />
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -1,262 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useFormStatus } from 'react-dom';
|
||||
|
||||
import { Database } from '@kit/supabase/database';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@kit/ui/dialog';
|
||||
import { ErrorBoundary } from '@kit/ui/error-boundary';
|
||||
import { Heading } from '@kit/ui/heading';
|
||||
import { Input } from '@kit/ui/input';
|
||||
import { Label } from '@kit/ui/label';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { deleteTeamAccountAction } from '../actions/delete-team-account-server-actions';
|
||||
import { leaveTeamAccountAction } from '../actions/leave-team-account-server-actions';
|
||||
|
||||
type AccountData =
|
||||
Database['public']['Functions']['organization_account_workspace']['Returns'][0];
|
||||
|
||||
export function TeamAccountDangerZone({
|
||||
account,
|
||||
userId,
|
||||
}: React.PropsWithChildren<{
|
||||
account: AccountData;
|
||||
userId: string;
|
||||
}>) {
|
||||
const isPrimaryOwner = userId === account.primary_owner_user_id;
|
||||
|
||||
if (isPrimaryOwner) {
|
||||
return <DeleteOrganizationContainer account={account} />;
|
||||
}
|
||||
|
||||
return <LeaveOrganizationContainer account={account} />;
|
||||
}
|
||||
|
||||
function DeleteOrganizationContainer(props: { account: AccountData }) {
|
||||
return (
|
||||
<div className={'flex flex-col space-y-4'}>
|
||||
<div className={'flex flex-col space-y-1'}>
|
||||
<Heading level={6}>
|
||||
<Trans i18nKey={'teams:deleteOrganization'} />
|
||||
</Heading>
|
||||
|
||||
<p className={'text-sm text-gray-500'}>
|
||||
<Trans
|
||||
i18nKey={'teams:deleteOrganizationDescription'}
|
||||
values={{
|
||||
organizationName: props.account.name,
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Dialog>
|
||||
<DialogTrigger>
|
||||
<Button
|
||||
data-test={'delete-organization-button'}
|
||||
type={'button'}
|
||||
variant={'destructive'}
|
||||
>
|
||||
<Trans i18nKey={'teams:deleteOrganization'} />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<Trans i18nKey={'teams:deletingOrganization'} />
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<DeleteOrganizationForm
|
||||
name={props.account.name}
|
||||
id={props.account.id}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DeleteOrganizationForm({ name, id }: { name: string; id: string }) {
|
||||
return (
|
||||
<ErrorBoundary fallback={<DeleteOrganizationErrorAlert />}>
|
||||
<form
|
||||
className={'flex flex-col space-y-4'}
|
||||
action={deleteTeamAccountAction}
|
||||
>
|
||||
<div className={'flex flex-col space-y-2'}>
|
||||
<div
|
||||
className={
|
||||
'border-2 border-red-500 p-4 text-sm text-red-500' +
|
||||
' flex flex-col space-y-2'
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<Trans
|
||||
i18nKey={'teams:deleteOrganizationDisclaimer'}
|
||||
values={{
|
||||
organizationName: name,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={'text-sm'}>
|
||||
<Trans i18nKey={'common:modalConfirmationQuestion'} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="hidden" value={id} name={'id'} />
|
||||
|
||||
<Label>
|
||||
<Trans i18nKey={'teams:organizationNameInputLabel'} />
|
||||
|
||||
<Input
|
||||
name={'name'}
|
||||
data-test={'delete-organization-input-field'}
|
||||
required
|
||||
type={'text'}
|
||||
className={'w-full'}
|
||||
placeholder={''}
|
||||
pattern={name}
|
||||
/>
|
||||
|
||||
<span className={'text-xs'}>
|
||||
<Trans i18nKey={'teams:deleteOrganizationInputField'} />
|
||||
</span>
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div className={'flex justify-end space-x-2.5'}>
|
||||
<DeleteOrganizationSubmitButton />
|
||||
</div>
|
||||
</form>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
function DeleteOrganizationSubmitButton() {
|
||||
const { pending } = useFormStatus();
|
||||
|
||||
return (
|
||||
<Button
|
||||
data-test={'confirm-delete-organization-button'}
|
||||
disabled={pending}
|
||||
variant={'destructive'}
|
||||
>
|
||||
<Trans i18nKey={'teams:deleteOrganization'} />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function LeaveOrganizationContainer(props: { account: AccountData }) {
|
||||
return (
|
||||
<div className={'flex flex-col space-y-4'}>
|
||||
<p>
|
||||
<Trans
|
||||
i18nKey={'teams:leaveOrganizationDescription'}
|
||||
values={{
|
||||
organizationName: props.account.name,
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<Dialog>
|
||||
<DialogTrigger>
|
||||
<Button
|
||||
data-test={'leave-organization-button'}
|
||||
type={'button'}
|
||||
variant={'destructive'}
|
||||
>
|
||||
<Trans i18nKey={'teams:leaveOrganization'} />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<Trans i18nKey={'teams:leavingOrganizationModalHeading'} />
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<ErrorBoundary fallback={<LeaveOrganizationErrorAlert />}>
|
||||
<form action={leaveTeamAccountAction}>
|
||||
<input type={'hidden'} value={props.account.id} name={'id'} />
|
||||
|
||||
<div className={'flex flex-col space-y-4'}>
|
||||
<div>
|
||||
<div>
|
||||
<Trans
|
||||
i18nKey={'teams:leaveOrganizationDisclaimer'}
|
||||
values={{
|
||||
organizationName: props.account?.name,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={'flex justify-end space-x-2.5'}>
|
||||
<LeaveOrganizationSubmitButton />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</ErrorBoundary>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LeaveOrganizationSubmitButton() {
|
||||
const { pending } = useFormStatus();
|
||||
|
||||
return (
|
||||
<Button
|
||||
data-test={'confirm-leave-organization-button'}
|
||||
disabled={pending}
|
||||
variant={'destructive'}
|
||||
>
|
||||
<Trans i18nKey={'teams:leaveOrganization'} />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function LeaveOrganizationErrorAlert() {
|
||||
return (
|
||||
<Alert variant={'destructive'}>
|
||||
<AlertTitle>
|
||||
<Trans i18nKey={'teams:leaveOrganizationErrorHeading'} />
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
<Trans i18nKey={'common:genericError'} />
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
function DeleteOrganizationErrorAlert() {
|
||||
return (
|
||||
<Alert variant={'destructive'}>
|
||||
<AlertTitle>
|
||||
<Trans i18nKey={'teams:deleteOrganizationErrorHeading'} />
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
<Trans i18nKey={'common:genericError'} />
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user