diff --git a/resources/js/pages/clients/invite.tsx b/resources/js/pages/clients/invite.tsx index eb96ad47..c76814d2 100644 --- a/resources/js/pages/clients/invite.tsx +++ b/resources/js/pages/clients/invite.tsx @@ -1,6 +1,6 @@ import { type BreadcrumbItem } from '@/types'; import { Head, router, useForm } from '@inertiajs/react'; -import { FormEventHandler } from 'react'; +import { FormEventHandler, useState } from 'react'; import { ConfirmDialog } from '@/components/confirm-dialog'; import Heading from '@/components/heading'; @@ -16,6 +16,8 @@ import { useFormatDate } from '@/hooks/use-format-date'; import { useTranslation } from '@/hooks/use-translation'; import AppLayout from '@/layouts/app-layout'; +type Tab = 'send' | 'pending'; + interface InvitationFormData { [key: string]: string; email: string; @@ -46,6 +48,10 @@ interface ClientsInviteProps { export default function ClientsInvite({ groups, default_storage_quota_mb, invitations, pagination }: ClientsInviteProps) { const { t } = useTranslation(); const { dateTime } = useFormatDate(); + // ?tab=pending opens on the list, so a link can point at the half it + // means — the same reason the theming settings read their own tab from + // the query string. Anything unrecognised falls back to the form. + const [tab, setTab] = useState(new URLSearchParams(window.location.search).get('tab') === 'pending' ? 'pending' : 'send'); const breadcrumbs: BreadcrumbItem[] = [ { title: t('Clients'), href: '/clients' }, @@ -73,7 +79,28 @@ export default function ClientsInvite({ groups, default_storage_quota_mb, invita
-
+ + + {/* Hidden rather than unmounted, the same as the staff account + form's tabs: switching to the list and back must not throw + away a half-typed invitation. */} +
-
- + {tab === 'pending' && ( +
+

+ {t('Links that have been sent and not used yet. Revoking one stops it working for good.')} +

- {t('No invitations are waiting to be used.')}} - > - {invitations.map((invitation) => ( - - - {invitation.email} - {invitation.name && {invitation.name}} - - {invitation.group ?? '—'} - {invitation.invited_by ?? '—'} - {dateTime(invitation.created_at)} - - {invitation.expired ? {t('Expired')} : dateTime(invitation.expires_at)} - - -
- - {t('Revoke')} - - } - title={t('Revoke this invitation?')} - description={t( - 'The link sent to :email stops working, and cannot be renewed by whoever holds it. You can send a new invitation at any time.', - { email: invitation.email }, - )} - confirmLabel={t('Revoke')} - onConfirm={() => router.delete(route('invitations.destroy', invitation.id))} - /> -
- - - ))} -
+ {t('No invitations are waiting to be used.')}} + > + {invitations.map((invitation) => ( + + + {invitation.email} + {invitation.name && {invitation.name}} + + {invitation.group ?? '—'} + {invitation.invited_by ?? '—'} + {dateTime(invitation.created_at)} + + {invitation.expired ? {t('Expired')} : dateTime(invitation.expires_at)} + + +
+ + {t('Revoke')} + + } + title={t('Revoke this invitation?')} + description={t( + 'The link sent to :email stops working, and cannot be renewed by whoever holds it. You can send a new invitation at any time.', + { email: invitation.email }, + )} + confirmLabel={t('Revoke')} + // preserveState so the page comes back on this tab + // rather than on the form: revoking redirects back + // here, and landing on the other half after acting + // on this one reads as having lost the list. + onConfirm={() => + router.delete(route('invitations.destroy', invitation.id), { + preserveState: true, + preserveScroll: true, + }) + } + /> +
+ + + ))} +
- -
+ +
+ )}
);