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:
Giancarlo Buomprisco
2026-03-11 14:45:42 +08:00
committed by GitHub
parent ca585e09be
commit 4bc8448a1d
530 changed files with 14398 additions and 11198 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>
);

View File

@@ -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,

View File

@@ -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>
);

View File

@@ -24,7 +24,7 @@ export function SignOutInvitationButton(
window.location.assign(safePath);
}}
>
<Trans i18nKey={'teams:signInWithDifferentAccount'} />
<Trans i18nKey={'teams.signInWithDifferentAccount'} />
</Button>
);
}

View File

@@ -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>
);